Three months after getting this beautiful model for my birthday, I’m calling it finished. Here’s my Dread Saurian.
I ran into an issue with my side project AoS Reminders recently where I was mutating an object that wasn’t supposed to be mutated.
I was having trouble tracking down the mutation, until I stumbled upon the Proxy
object, a helpful tool for any Javascript developer.
const obj = { key1: 'value', key2: 'value2' }
const handler = {
get(target, key) {
console.log('Reading value from ' + key)
return target[key]
},
}
const proxiedObj = new Proxy(obj, handler)
// When we run this `if` statement, console will log
// 'Reading value from key1'
if (proxiedObj.key1) { }
This can be useful for detecting when your objects are being accessed.
Another great example of using Proxy
is for object validation:
// Use the proxy handler as a validator
const validator = {
set: (obj, prop, value) => {
// Validate that we passed in a correct `type` value
if (prop === 'type') {
const validTypes = ['Mage', 'Barbarian', 'Archer']
if (value === null) {
throw new TypeError('Cannot set to type to null');
}
if (!validTypes.includes(value)) {
throw new TypeError('Cannot set to type to ' + value);
}
}
// Store the new value in your object
obj[prop] = value;
// `set` expects a boolean response if successful
return true;
}
};
let char = new Proxy({age: 40, stars: 5}, validator);
char.name = 'Dennis' // Other keys are not validated
char.type = 'Golden God' // Throws `Cannot set to type to Golden God`
console.log(char.type) // undefined
char.type = 'Mage' // Passes validation
console.log(char.type) // 'Mage'
char.type = null; // Throws `Cannot set to type to null`
console.log(char.type) // 'Mage'
While useful, you should really be using Typescript and enforcing your object keys that way ;)
Hope this helps someone! Just thought I’d share a small, neat feature of JS I hadn’t used before.
In this post, I’d like to convince you to use Immer, 2019’s hottest JavaScript framework.
Immer is a tiny package that allows you to work with immutable state in a more convenient way.
The basic idea is that you will apply all your changes to a temporary draftState
, which is a proxy of the currentState
. Once all your mutations are completed, Immer will produce the nextState
based on the mutations to the draftState
. This means that you can interact with your data by simply modifying it while keeping all the benefits of immutable data.
We write a lot of boilerplate when using Redux and updating our store. Every developer familiar with even remotely complex reducers has seen this pattern before:
const someReducer = (state, action) => {
const newEntry = {
...state.entries[action.payload.id],
completed: true
}
return {
...state,
entries: {
...state.entries,
[action.payload.id]: newEntry
}
}
}
All that code just to set one entry to completed: true
! This “extra” destructuring work is required because Redux needs us to return a complete copy of the state.
What if we could just modify the entry we want, and Redux could figure out the rest? Well, that may be expecting too much of Redux, but luckily, we can now use Immer!
With Immer, the above code would look like:
import produce from "immer"
const someReducer = produce((state, action) => {
state.entries[action.payload.id].completed = true
})
From a developer point of view, I think the benefits are obvious. We’re no longer spending time trying to destructure and then restructure state. We’re just performing a simple operation and moving on with our lives.
To begin with, what does an Immer function even look like?
Here’s an example:
import produce from 'immer'
const firstState = {}
const secondState = produce(firstState, draft => {
// empty function
})
console.log(secondState === firstState) // true
// Let's begin using Immer by adding a { 'key': 'value' } pair to the state
const thirdState = produce(firstState, draft => {
draft['key'] = 'value'
})
console.log(firstState) // {}
console.log(thirdState) // { key: 'value' }
// Now we get to see the magic of Immer
const fourthState = produce(thirdState, draft => {
delete draft['key']
draft['newKey'] = { one: 'two' }
draft['rubicon'] = 'awesome'
})
// thirdState is unmodified, of course!
console.log(thirdState) // { key: 'value' }
console.log(fourthState)
// {
// newKey: { one: 'two' },
// rubicon: 'awesome'
// }
Okay, let’s do something a little more realistic. Let’s pretend we’re building an app where users can check a certain field. A todo list or grocery shopping app would make sense.
import produce from 'immer'
const state = [
{
id: 1,
checked: false
},
{
id: 2,
checked: false
}
]
const result = produce(state, draft => {
draft[0].checked = true
draft.push({ id: 3, checked: false })
})
console.log(result)
// [
// { id: 1, checked: true },
// { id: 2, checked: false },
// { id: 3, checked: false }
// ]
I think you’re probably starting to get the idea. But let’s get into why this would work for advanced applications where you’re using and abusing Redux heavily.
Here is a real example of a very painful piece of code that we had written at work. We have a UX flow that allows the user to change the height
of a displayed chart. The only thing this action handler wants to do is update the height
property of a specific chart. But unfortunately, we have to fully recreate the state via destructuring - and it gets ugly!
export const setChartHeightActionHandler = (state, { meta }) => {
const { chartId, height } = meta
const { byId } = state
const chartObject = { ...byId[chartId] }
const { chartDimensions } = chartObject
const thisChartDimensions = chartDimensions[chartId]
return {
...state,
byId: {
...byId,
[chartId]: {
...chartObject,
chartDimensions: {
...chartDimensions,
[chartId]: {
...thisChartDimensions,
height // This is literally the only change made
}
}
}
}
}
}
Ugh! That is pretty gross. Let’s use Immer instead.
import produce from "immer"
export const setChartHeightActionHandler = produce((state, { meta }) => {
const { chartId, height } = meta
state.byId[chartId].chartDimensions[chartId].height = height
})
Boom! The exact same functionality as above.
(If this example doesn’t win you over, I have nothing to offer you.)
Guess which one is easier to understand? Guess which one doesn’t take 15 minutes and two developers to write? (True story).
Let’s refactor another action handler - this one deletes a single entry in the store.
export const deleteExperimentActionHandler = (state, { meta }) => {
const { experimentId, projectId } = meta
if (!state.byId[experimentId]) return state
const byId = {
...state.byId
}
delete byId[experimentId] // The only operation actually needed
return {
...state,
byProject: {
[projectId]: Object.values(byId)
},
byId
}
}
import produce from "immer"
export const deleteExperimentActionHandler = produce((state, { meta }) => {
// Returning undefined is the same as saying "keep the same state"
if (!state.byId[meta.experimentId]) return
delete state.byId[meta.experimentId]
})
Is that super dope or what? We accomplish the same task with two lines of code.
Immer does have a downside - namely, it’s about twice as slow as a well-written reducer. Immer recommends not using this library when operating on tens of thousands of objects - otherwise, the performance difference is neglible.
Use Immer wherever it makes sense to you. It’s easy to start with, and the developer benefits will become clear immediately. Your functions will also become easier to test and debug, since you’ll be rid of the noise that comes along with re-creating state all the time!
For those of you using TypeScript - the Immer library is strongly typed! Very cool!
If you’ve ever worked with a multi-page React app, you will invariably run into the dreaded warning in your console:
Warning: Can't perform a React state update on an unmounted component.
The most common cause of this warning is when a user kicks off an asynchronous request, but leaves the page before it completes. I especially wanted to know how to do this with hooks, because hooks are awesome.
I wanted to know how to avoid this classic React warning - here’s my solution.
const SampleComponent = () => {
const [counter, setCounter] = useState(0)
// Here's how we'll keep track of our component's mounted state
const componentIsMounted = useRef(true)
useEffect(() => {
return () => {
componentIsMounted.current = false
}
}. []) // Using an empty dependency array ensures this only runs on unmount
const incrementCounterAsync = useCallback(()=> {
async () => {
try {
await someLongRunningProcess()
if (componentIsMounted.current) {
setCounter(counter + 1)
}
} catch (err) {
// Handle your error
}
}
}, [setCounter])
render (
// Display the counter + a button to increment it
)
}
To explain the above code snippet, imagine we have a sample component that simply displays a counter, and a button to increment the counter.
When we click the button, it fires off someLongRunningProcess()
- this could be a backend operation or whatever. Use your imagination.
When someLongRunningProcess()
has completed, we make sure we’re still mounted via the componentIsMounted
variable! If the component is still mounted, we can modify the state, no problem. If it’s not mounted - hey, that’s alright, we won’t touch the state.
This will dodge that nasty React warning! Have fun!