Kyle Shevlin

Software Engineer
January 19, 2022
0 strokes bestowed

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.


Finished reading?

Liked the post? Give the author a dopamine boost with a few "beard strokes". Click it up to 50 times show your appreciation.

Related Post:
Prefer Declarative State Updaters
Tags
JavaScriptReact

Are you, or the company you work for, struggling with something mentioned in this article?
Would you benefit from a live training session?
Let's Talk
Kyle Shevlin's face, which is mostly a beard with eyes
Kyle Shevlin is a software engineer who specializes in JavaScript, TypeScript, React and frontend web development.

Let's talk some more about JavaScript, TypeScript, React, and software engineering.

I write a newsletter to share my thoughts and the projects I'm working on. I would love for you to join the conversation. You can unsubscribe at any time.

Array.reduce() Logo
Array.reduce()

Check out my courses!

Liked the post? You might like my courses, too. Click the button to view this course or go to Courses for more information.
View the course
I would like give thanks to those who have contributed fixes and updates to this blog. If you see something that needs some love, you can join them. This blog is open sourced at https://github.com/kyleshevlin/blog
alexloudenjacobwsmithbryndymentJacobMGEvanseclectic-codingjhsukgcreativeerikvorhesHaroenvmarktnoonandependabotmarcuslyonsbrentmclarkfederico-fiorinimedayzTowardsDeathFanchGadjonoahmateenbrandonpittmanmattridley
©2023 Kyle Shevlin. All Rights Reserved.