Don't Sync State. Derive It!

Photo by Gabriel Gusmao

How to avoid state synchronization bugs and complexity with derived state.

In my Learn React Hooks workshop material, we have an exercise where we build a tic-tac-toe game using React's useState hook (based on the official React tutorial). Here's the finished version of that exercise:

We have a few variables of state. There's a squares state variable via React.useState . There's also nextValue , winner , and status are each determined by calling the functions calculateNextValue , calculateWinner , and calculateStatus . squares is regular component state, but nextValue , winner , and status are what are called "derived state." That means that their value can be derived (or calculated) based on other values rather than managed on their own.

There's a good reason that I wrote it the way I did. Let's find out the benefits of derived state over state synchronization by reimplementing this with a more naive approach. The fact is that all four variables are technically state so you may automatically think that you need to use useState or useReducer for them.

Let's start with useState :

1 function Board ( ) { 2 const [ squares , setSquares ] = React . useState ( Array ( 9 ) . fill ( null ) ) 3 const [ nextValue , setNextValue ] = React . useState ( calculateNextValue ( squares ) ) 4 const [ winner , setWinner ] = React . useState ( calculateWinner ( squares ) ) 5 const [ status , setStatus ] = React . useState ( calculateStatus ( squares ) ) 6 7 function selectSquare ( square ) { 8 if ( winner || squares [ square ] ) { 9 return 10 } 11 const squaresCopy = [ ... squares ] 12 squaresCopy [ square ] = nextValue 13 const newNextValue = calculateNextValue ( squaresCopy ) 14 const newWinner = calculateWinner ( squaresCopy ) 15 const newStatus = calculateStatus ( newWinner , squaresCopy , newNextValue ) 16 setSquares ( squaresCopy ) 17 setNextValue ( newNextValue ) 18 setWinner ( newWinner ) 19 setStatus ( newStatus ) 20 } 21 22 23 }

So that's not all that bad. Where it becomes a real problem is what if we added a feature to our tic-tac-toe game where you could select two squares at once? What would we have to do to make that happen?

1 function Board ( ) { 2 const [ squares , setSquares ] = React . useState ( Array ( 9 ) . fill ( null ) ) 3 const [ nextValue , setNextValue ] = React . useState ( calculateNextValue ( squares ) ) 4 const [ winner , setWinner ] = React . useState ( calculateWinner ( squares ) ) 5 const [ status , setStatus ] = React . useState ( calculateStatus ( squares ) ) 6 7 function selectSquare ( square ) { 8 if ( winner || squares [ square ] ) { 9 return 10 } 11 const squaresCopy = [ ... squares ] 12 squaresCopy [ square ] = nextValue 13 14 const newNextValue = calculateNextValue ( squaresCopy ) 15 const newWinner = calculateWinner ( squaresCopy ) 16 const newStatus = calculateStatus ( newWinner , squaresCopy , newNextValue ) 17 setSquares ( squaresCopy ) 18 setNextValue ( newNextValue ) 19 setWinner ( newWinner ) 20 setStatus ( newStatus ) 21 } 22 23 function selectTwoSquares ( square1 , square2 ) { 24 if ( winner || squares [ square1 ] || squares [ square2 ] ) { 25 return 26 } 27 const squaresCopy = [ ... squares ] 28 squaresCopy [ square1 ] = nextValue 29 squaresCopy [ square2 ] = nextValue 30 31 const newNextValue = calculateNextValue ( squaresCopy ) 32 const newWinner = calculateWinner ( squaresCopy ) 33 const newStatus = calculateStatus ( newWinner , squaresCopy , newNextValue ) 34 setSquares ( squaresCopy ) 35 setNextValue ( newNextValue ) 36 setWinner ( newWinner ) 37 setStatus ( newStatus ) 38 } 39 40 41 }

The biggest problem with this is some of that state may fall out of sync with the true component state ( squares ). It could fall out of sync because we forget to update it for a complex sequence of interactions for example. If you've been building React apps for a while, you know what I'm talking about. It's no fun to have things fall out of sync.

One thing that can help is to reduce duplication so that all relevant state updates happen in one place:

1 function Board ( ) { 2 const [ squares , setSquares ] = React . useState ( Array ( 9 ) . fill ( null ) ) 3 const [ nextValue , setNextValue ] = React . useState ( calculateNextValue ( squares ) ) 4 const [ winner , setWinner ] = React . useState ( calculateWinner ( squares ) ) 5 const [ status , setStatus ] = React . useState ( calculateStatus ( squares ) ) 6 7 function setNewState ( newSquares ) { 8 const newNextValue = calculateNextValue ( newSquares ) 9 const newWinner = calculateWinner ( newSquares ) 10 const newStatus = calculateStatus ( newWinner , newSquares , newNextValue ) 11 setSquares ( newSquares ) 12 setNextValue ( newNextValue ) 13 setWinner ( newWinner ) 14 setStatus ( newStatus ) 15 } 16 17 function selectSquare ( square ) { 18 if ( winner || squares [ square ] ) { 19 return 20 } 21 const squaresCopy = [ ... squares ] 22 squaresCopy [ square ] = nextValue 23 setNewState ( squaresCopy ) 24 } 25 26 function selectTwoSquares ( square1 , square2 ) { 27 if ( winner || squares [ square1 ] || squares [ square2 ] ) { 28 return 29 } 30 const squaresCopy = [ ... squares ] 31 squaresCopy [ square1 ] = nextValue 32 squaresCopy [ square2 ] = nextValue 33 setNewState ( squaresCopy ) 34 } 35 36 37 }

That's really improved our code duplication, and it wasn't that big of a deal honestly. But this is a pretty simple example. Sometimes the derived state is based on multiple variables of state that are updated in different situations and we need to make sure that all our state is updated whenever the source state is updated.

The solution

What if I told you there's something better? If you've already read through the codesandbox implementation above, you know what that solution is, but let's put it right here now:

1 function Board ( ) { 2 const [ squares , setSquares ] = React . useState ( Array ( 9 ) . fill ( null ) ) 3 const nextValue = calculateNextValue ( squares ) 4 const winner = calculateWinner ( squares ) 5 const status = calculateStatus ( winner , squares , nextValue ) 6 7 function selectSquare ( square ) { 8 if ( winner || squares [ square ] ) { 9 return 10 } 11 const squaresCopy = [ ... squares ] 12 squaresCopy [ square ] = nextValue 13 setSquares ( squaresCopy ) 14 } 15 16 17 }

Nice! We don't need to worry about updating the derived state values because they're simply calculated every render. Cool. Let's add that two squares at a time feature:

1 function Board ( ) { 2 const [ squares , setSquares ] = React . useState ( Array ( 9 ) . fill ( null ) ) 3 const nextValue = calculateNextValue ( squares ) 4 const winner = calculateWinner ( squares ) 5 const status = calculateStatus ( winner , squares , nextValue ) 6 7 function selectSquare ( square ) { 8 if ( winner || squares [ square ] ) { 9 return 10 } 11 const squaresCopy = [ ... squares ] 12 squaresCopy [ square ] = nextValue 13 setSquares ( squaresCopy ) 14 } 15 16 function selectTwoSquares ( square1 , square2 ) { 17 if ( winner || squares [ square1 ] || squares [ square2 ] ) { 18 return 19 } 20 const squaresCopy = [ ... squares ] 21 squaresCopy [ square1 ] = nextValue 22 squaresCopy [ square2 ] = nextValue 23 setSquares ( squaresCopy ) 24 } 25 26 27 }

Sweet! Before we had to concern ourselves with every single time we updated the squares state to ensure we updated all of the other state properly as well. But now we don't need to worry about it at all. It just works. No need for a fancy function to handle updating all the derived state. We just calculate it on the fly.

What about useReducer ?

useReducer doesn't suffer as badly from these problems. Here's how I might implement this using useReducer :

1 function calculateDerivedState ( squares ) { 2 const winner = calculateWinner ( squares ) 3 const nextValue = calculateNextValue ( squares ) 4 const status = calculateStatus ( winner , squares , nextValue ) 5 return { squares , nextValue , winner , status } 6 } 7 8 function ticTacToeReducer ( state , square ) { 9 if ( state . winner || state . squares [ square ] ) { 10 11 12 return state 13 } 14 15 const squaresCopy = [ ... state . squares ] 16 squaresCopy [ square ] = state . nextValue 17 18 return { ... calculateDerivedState ( squaresCopy ) , squares : squaresCopy } 19 } 20 21 function Board ( ) { 22 const [ { squares , status } , selectSquare ] = React . useReducer ( 23 ticTacToeReducer , 24 Array ( 9 ) . fill ( null ) , 25 calculateDerivedState , 26 ) 27 28 29 }

This isn't the only way to do this, but the point here is that while we do still "derive" state for winner , nextValue , and status , we're managing all of that within the reducer which is the only place state updates can happen, so falling out of sync is less likely.

That said, I find this to be a little more complex than our other solution (especially if we want to add that "two squares at a time" feature). So if I were building and shipping this in a production app, I'd go with what I've got in that codesandbox.

Derived state via props

State doesn't have to be managed internally to suffer from the state synchronization problems. What if we had the squares state coming from a parent component? How would we synchronize that state?

1 function Board ( { squares , onSelectSquare } ) { 2 const [ nextValue , setNextValue ] = React . useState ( calculateNextValue ( squares ) ) 3 const [ winner , setWinner ] = React . useState ( calculateWinner ( squares ) ) 4 const [ status , setStatus ] = React . useState ( calculateStatus ( squares ) ) 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 }

The better way to do this is just to calculate it on the fly:

1 function Board ( { squares , onSelectSquare } ) { 2 const nextValue = calculateNextValue ( squares ) 3 const winner = calculateWinner ( squares ) 4 const status = calculateStatus ( squares ) 5 6 7 }

It's way simpler, and it works really well.

P.S. Remember getDerivedStateFromProps ? Well you probably don't need it but if you do and you want to do so with hooks, then calling the state updater function during render is actually the correct way to do it. Learn more from the React Hooks FAQ.

What about performance?

I know you've been waiting for me to address this... Here's the deal. JavaScript is really fast. I ran a benchmark on the calculateWinner function and this resulted in 15 MILLION operations per second. So unless your tic-tac-toe players are extremely fast at clicking around, there's no way this is going to be a performance problem (and even if they could play that fast, I assure you that you'll have other performance problems that will be lower hanging fruit for you).

Ok ok, I tried it on my phone and only got 4.3 million operations per second. And then I tried with a CPU 6x slowdown on my laptop and only got 2 million... I think we're still good.

That said, if you do happen to have a function which is computationally expensive, then that's what useMemo is for!

1 function Board ( ) { 2 const [ squares , setSquares ] = React . useState ( Array ( 9 ) . fill ( null ) ) 3 const nextValue = React . useMemo ( ( ) => calculateNextValue ( squares ) , [ squares ] ) 4 const winner = React . useMemo ( ( ) => calculateWinner ( squares ) , [ squares ] ) 5 const status = React . useMemo ( 6 ( ) => calculateStatus ( winner , squares , nextValue ) , 7 [ winner , squares , nextValue ] , 8 ) 9 10 11 }

So there you go. An escape hatch for you to use once you've determined that some code is actually computationally expensive for your users to run. Note that this doesn't magically make those functions run faster. All it does is ensure that they're not called unnecessarily. If this were our whole app, the only way for the app to re-render is if squares changes in which case all of those functions will be run anyway, so we've actually not accomplished much with this "optimization." That's why I say: "Measure first!"

Oh, and I'd like to mention that derived state can sometimes be even faster than state synchronization because it will result in fewer unnecessary re-renders, which can be a problem sometimes.

What about MobX/Reselect?

Reselect (which you should absolutely be using if you're using Redux) has memoization built-in which is cool. MobX has this as well, but they also take it a step further with "computed values" which is basically an API to give you memoized and optimized derived state values. What makes it even better than what we already have is that the computation is only processed when it's accessed.

For (contrived) example:

1 function FavoriteNumber ( ) { 2 const [ name , setName ] = React . useState ( '' ) 3 const [ number , setNumber ] = React . useState ( 0 ) 4 const numberWarning = getNumberWarning ( number ) 5 return ( 6 < div > 7 < label > 8 Your name : < input onChange = { e => setName ( e . target . value ) } /> 9 </ label > 10 < label > 11 Your favorite number : { ' ' } 12 < input 13 type = " number " 14 onChange = { e => setNumber ( Number ( e . target . value ) ) } 15 /> 16 </ label > 17 < div > 18 { name 19 ? ` ${ name } 's favorite number is ${ number } ` 20 : 'Please type your name' } 21 </ div > 22 < div > { number > 10 ? numberWarning : null } </ div > 23 < div > { number < 0 ? numberWarning : null } </ div > 24 </ div > 25 ) 26 }

Notice that we're calling getNumberWarning , but we're only using the result if the number is too high or too low, so we may not actually need to call that function at all. Now, it's unlikely this is problematic, but let's say for the sake of argument that calling getNumberWarning is an application bottleneck. This is where the computed values feature comes in handy.

If you're experiencing this a lot in your app, then I suggest you just jump into using MobX (MobX folks will tell you there are a lot of other reasons to use it as well), but we can solve this specific situation pretty easily ourselves:

1 function FavoriteNumber ( ) { 2 const [ name , setName ] = React . useState ( '' ) 3 const [ number , setNumber ] = React . useState ( 0 ) 4 const numberIsTooHigh = number > 10 5 const numberIsTooLow = number < 0 6 const numberWarning = 7 numberIsTooHigh || numberIsTooLow ? getNumberWarning ( number ) : null 8 return ( 9 < div > 10 < label > 11 Your name : < input onChange = { e => setName ( e . target . value ) } /> 12 </ label > 13 < label > 14 Your favorite number : { ' ' } 15 < input 16 type = " number " 17 onChange = { e => setNumber ( Number ( e . target . value ) ) } 18 /> 19 </ label > 20 < div > 21 { name 22 ? ` ${ name } 's favorite number is ${ number } ` 23 : 'Please type your name' } 24 </ div > 25 < div > { numberIsTooHigh ? numberWarning : null } </ div > 26 < div > { numberIsTooLow ? numberWarning : null } </ div > 27 </ div > 28 ) 29 }

Great! Now we don't need to worry about calling numberWarning when it's not needed. But if that doesn't work well for your situation, then we could make a custom hook do this magic for us. It's not exactly simple and it's a bit of a hack (there's probably a better way to do it honestly), so I'm just going to put this in a codesandbox and let you explore it if you want:

It's sufficient to say that the custom hook allows us to do this:

1 function FavoriteNumber ( ) { 2 const [ name , setName ] = React . useState ( '' ) 3 const [ number , setNumber ] = React . useState ( 0 ) 4 const numberWarning = useComputedValue ( ( ) => getNumberWarning ( number ) , [ 5 number , 6 ] ) 7 return ( 8 < div > 9 < label > 10 Your name : < input onChange = { e => setName ( e . target . value ) } /> 11 </ label > 12 < label > 13 Your favorite number : { ' ' } 14 < input 15 type = " number " 16 onChange = { e => setNumber ( Number ( e . target . value ) ) } 17 /> 18 </ label > 19 < div > 20 { name 21 ? ` ${ name } 's favorite number is ${ number } ` 22 : 'Please type your name' } 23 </ div > 24 < div > { number > 10 ? numberWarning . result : null } </ div > 25 < div > { number < 0 ? numberWarning . result : null } </ div > 26 </ div > 27 ) 28 }

And our getNumberWarning function is only called when the result is actually used. Think of it like a useMemo that only runs the callback when the return value is rendered.

I think there may be room to perfect and open source that. Feel free to do so and then make a PR to this blog post to add a link to your published package 😉

Again, there's really not much reason to worry yourself over this kind of thing in a normal scenario. But if you do have perf bottlenecks around and useMemo isn't enough for you, then consider doing something like this or use MobX.

Conclusion

Ok, so we got a little distracted overthinking performance for a second there. The fact is that you can really simplify your app's state by considering whether the state needs to be managed by itself or if it can be derived. We learned that derived state can be the result of a single variable of state, or it can be derived from multiple variables of state (some of which can also be derived state itself).

So next time you're maintaining the state of your app and trying to figure out a synchronization bug, think about how you could make it derived on the fly instead. And in the few instances you bump into performance issues you can reach to a few optimization strategies to help alleviate some of that pain. Good luck!