The Wrapped State Setter Pattern
Today, in the process of working on what might potentially become a course on React, I used a pattern that I’ve found useful from time to time that I want to share with you.
Have you ever wanted to have something happen every time you use setState
in React? Perhaps you wanted a way to finagle the nextState
or wanted to run a side effect where you store the value in localStorage
. We can accomplish this and more with a “wrapped state setter” pattern.
Since my example app for the potential course uses localStorage
in lieu of a database, I’ll use that for my example code. That said, the pattern is the same regardless of what you use it for, so let’s dive in.
Let’s say my app stores a collection of notes:
function App() {
const [notes, setNotes] = React.useState([])
//...
}
Now, let’s add some declarative state updaters that will update our notes. Keep in mind, there are multiple ways to write these updaters. Don’t get too focused on the implementation details, just recognize we’ve written a few. Like so:
function App() {
const [notes, setNotes] = React.useState([])
const createNote = note => {
setNotes(currentNotes => [...currentNotes, note])
}
const updateNote = update => {
setNotes(currentNotes => {
const index = currentNotes.findIndex(note => note.id === update.id)
if (index < 0) return currentNotes
return [
...currentNotes.slice(0, index),
{ ...currentNotes[index], ...update },
...currentNotes.slice(index + 1),
]
})
}
const deleteNote = id => {
setNotes(currentNotes => {
const index = currentNotes.findIndex(note => note.id === id)
if (index < 0) return currentNotes
return [...currentNotes.slice(0, index), ...currentNotes.slice(index + 1)]
})
}
//...
}
Now, let’s say that we want to store these changes to notes
in localStorage
every time we make an update. What is the simplest way to do this?
If we look at all our declarative updaters, they all make a call to setNotes
. If we had a way to change setNotes
, we could make this happen. This is where the “wrapped” part of the title comes in.
We’re going to create a function that will call the current setNotes
and use it to provide ourselves with a seam in which to extend the current behavior. Let’s wrap it up, like so:
const wrappedSetNotes = React.useCallback(update => {
// Call the original state setter, use the function updater to get the current state
setNotes(currentNotes => {
// Determine the nextState
const nextState =
typeof update === 'function' ? update(currentNotes) : update
// This is where we extend the functionality
localStorage.setItem(YOUR_LOCAL_STORAGE_KEY, JSON.stringify(nextState))
// We preserve the existing functionality
return nextState
})
}, [])
And then every where we previously called setNotes
, we update to wrappedSetNotes
:
const createNote = note => {
wrappedSetNotes(currentNotes => [...currentNotes, note])
}
const updateNote = update => {
wrappedSetNotes(currentNotes => {
const index = currentNotes.findIndex(note => note.id === update.id)
if (index < 0) return currentNotes
return [
...currentNotes.slice(0, index),
{ ...currentNotes[index], ...update },
...currentNotes.slice(index + 1),
]
})
}
const deleteNote = id => {
wrappedSetNotes(currentNotes => {
const index = currentNotes.findIndex(note => note.id === id)
if (index < 0) return currentNotes
return [...currentNotes.slice(0, index), ...currentNotes.slice(index + 1)]
})
}
Now, any updates that we make to notes
will have the side effect of updating the value in localStorage
.
There are other ways to use this pattern. If you’ve ever heard of the “state reducer pattern”, that’s an example of wrapping the state setter.
This pattern also doesn’t have to be just for state setters. If ever you find yourself with a function that you want to add some functionality to, you might be able to simply wrap it in a function, and add your functionality in the function body. Try it out, see if it works.