January 19, 2022

The Wrapped State Setter Pattern

edit

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.


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Logo for Array.reduce()
Array.reduce()
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.