Upcoming Workshops
3-part Series
- How is state related to the declarative approach in React?
- React State: Common Questions?
- ⭐️ Derived State.
The useState
and useReducer
hooks are React's primitives for making state. However, you shouldn't think of them as being the only way to do state in React (and I'm not talking about global state tools like Redux).
Instead, you should think of state as being any value that can change over time between re-rendering.
For example, the URL can be thought of as "state". The URL is the original "state" for the web. We can subscribe to changes in the URL with tools like React Router.
I've built shopping-cart experiences where you see a list of products similar to Amazon. Then there is a left sidebar where you can filter the list of products by brand, price, type, etc. There is quite a bit of distance in the React component tree between the Sidebar
component and the BrowseProducts
component so I need to consider where to keep my state so that the Sidebar
can update it and the browse page can read it.
Option One: I suppose I could "lift state" to some common ancestor component above both Sidebar
and BrowseProducts
. It would work but if the user wants to filter by a certain brand, I think it would be unfortunate if they refreshed or shared the URL with a friend and filter state is lost.
Option Two: If we had done this the "old school" way, we would have just tracked a ?brand=xyz"
query variable in the URL. Now refreshes would persist the "state" what we want to see between refreshes and shares. So does that mean we "sync" our lifted useState
state to the URL. I wouldn't do that. This is a form of "state duplication" and is going to lead to bugs. A React developer doing this is probably thinking that useState
is the only way to keep track of state over time. I think you should just use the URL state and forget about useState
in this case. Have the click events in Sidebar
just change the URL. Then use a tool like React Router's useLocation
in BrowseProducts
to get re-renders when the URL changes.
The point is, state in React is not limited to useState
and useReducer
.
So far though, this isn't what we mean by "derived state" but it's getting us a step closer by thinking outside the useState
box.
Derived State
Also sometimes known as "computed state" is when we derive a value for a variable upon re-render from some other state. This is best demonstrated with an example.
Consider this counter that currently does not have derived state:
function Counter() {const [count, setCount] = useState(0)function subtract() {setCount(count - 1)}function add() {setCount(count + 1)}return (<div><button onClick={subtract}>Subtract</button><div>{count}</div><button onClick={add}>Add</button></div>)}
If I were to want an error message for when the could is below 0, I might do this:
function Counter() {const [count, setCount] = useState(0)const [error, setError] = useState('')function subtract() {setCount(count - 1)if (count - 1 < 0) {setError('Cannot be less than 0')}}function add() {setCount(count + 1)if (count + 1 >= 0) {setError('')}}return (<div><button onClick={subtract}>Subtract</button><div>{count}</div><button onClick={add}>Add</button><p>{error}</p></div>)}
Now we have more state and we have to "manage" it with each event. It's a simple example but this management could be a lot more involved under other circumstances.
Now let's see what it looks like with derived state instead:
function Counter() {const [count, setCount] = useState(0)const error = count < 0 ? 'Cannot be less than 0' : ''function subtract() {setCount(count - 1)}function add() {setCount(count + 1)}return (<div><button onClick={subtract}>Subtract</button><div>{count}</div><button onClick={add}>Add</button><p>{error}</p></div>)}
Look at the subtract
and add
functions! No management needed. We don't really need to explicitly set an error from useState
, we can just derive on renders that the error variable is an error string or an empty string. This gives us the exact same behavior in our component but without as much complexity. It's also going to give us less bugs under circumstances where we try to manage state and do it poorly.
Summary
Always derive state where possible. It will make things cleaner, smaller, and easier to re-factor. Only use useState
and useReducer
if you really do need to explicitly set state and they give you the best option for doing so.