Managed vs. Derived State
In Epic React Kent takes us through the exercise of building a Tic Tac Toe game. Anyone who has tried learning React before has probably seen the Tic Tac Toe tutorial on the official docs. This is very similar, but it uses hooks instead of classes, which makes it a great learning experience.
Rather than go through the entire tutorial, the post I wanted to write was about managed vs. derived state.
Managed State
When I first started the exercise, I assumed every variable needed to have its own state managed somewhere.
For Tic Tac Toe, we need the current squares, a calculation of the current winner (if any), the next value, and the status (i.e. next player is 'X' or 'winner: X').
To do this with managed state, I first wrote code that looked like this:
function Board() {
const [squares, setSquares] = React.useState(Array(9).fill(null))
const [nextValue, setNextValue] = React.useState(calculateNextValue(squares))
const [winner, setWinner] = React.useState(calculateWinner(squares))
const [status, setStatus] = React.useState(calculateStatus(squares))
function selectSquare(square) {
if (winner || squares[square]) {
return
}
const squaresCopy = [...squares]
squaresCopy[square] = nextValue
setSquares(squaresCopy)
setNextValue(calculateNextValue(squaresCopy))
setWinner(calculateWinner(squaresCopy))
setStatus(calculateStatus(newWinner, squaresCopy, newNextValue)
)
}
// return beautiful JSX
}
A few things happening here:
We make a copy of the squares because you aren't supposed to mutate variables that are being managed in state. If we tried to use
setSquares
withsquares
and not...squares
we could risk falling out of sync.What I didn't realize was that directly calling
setNextValue
, etc with the function calls would also lead to sync problems. I believe this is becausesetState
is an async call, and so implementing it this way meant my state was always lagging by one.
To fix this, we can call the function calls and store the results in a variable. Later we can use those variables to update the state:
function Board() {
const [squares, setSquares] = React.useState(Array(9).fill(null))
const [nextValue, setNextValue] = React.useState(calculateNextValue(squares))
const [winner, setWinner] = React.useState(calculateWinner(squares))
const [status, setStatus] = React.useState(calculateStatus(squares))
function selectSquare(square) {
if (winner || squares[square]) {
return
}
const squaresCopy = [...squares]
squaresCopy[square] = nextValue
const newNextValue = calculateNextValue(squaresCopy)
const newWinner = calculateWinner(squaresCopy)
const newStatus = calculateStatus(newWinner, squaresCopy, newNextValue)
setSquares(squaresCopy)
setNextValue(newNextValue)
setWinner(newWinner)
setStatus(newStatus)
}
// return beautiful JSX
}
This is the managed state example. I don't have enough experience to know why this isn't ideal, but Kent explains it well here. The general gist is that as your application gets more complex, you're much more likely to fall out of sync. So the real solution--which I happen to this is quite elegant--is to derive state.
Derived State
If you think about it, we really don't need to make the 3 latter values stateful. They all depend on the original variable squares
which is being managed with state. So instead of tracking them with state, we could just have our application calculate them every time the state of squares
updates, thereby triggering a rerender:
function Board() {
const [squares, setSquares] = React.useState(Array(9).fill(null))
const nextValue = calculateNextValue(squares)
const winner = calculateWinner(squares)
const status = calculateStatus(winner, squares, nextValue)
function selectSquare(square) {
if (winner || squares[square]) {
return
}
const squaresCopy = [...squares]
squaresCopy[square] = nextValue
setSquares(squaresCopy)
}
// return beautiful JSX
}
In my mind this is very elegant and easy to follow, and makes it a lot easier to avoid issues.