Everyone loves an opportunity to slam on the big dog of frontend - React; but when it comes to state, it really is first class!
Let's talk about state and explore how to make managing it a breeze.
Literal forms of state
First off, it helps to understand the two forms that state can take in an application.
Explicit state
In the case of modern React, this is useState
and useReducer
. Explicit state doesn't just come out of thin air - it has to be explicitly created and managed.
Derived state
A psuedostate of sorts, derived state is a result of processing one or more states (explicit or derived).
const [input, setInput] = useState(); // Explicit state
const inputValid = useMemo( // Derived state
() => input && input.length > 6,
[input]
);
Choosing types of state
Knowing whether to use explicit or derived state might seem challenging - but there's a really simple answer.
Always use derived state where possible
Forgetting to stick with the above rule can lead to redundant state.
Unlike redundant code, redundant state is a real problem that actually exists; and can have an impact on everything from performance to maintainability.
Spotting redundancy
If you've ever written something like the following - I know I have - you've probably been guilty of creating redundant state.
const [value, setValue] = useState("");
const [isValid, setIsValid] = useState(false);
useEffect(
() => setIsValid(value && value.length > 4),
[value]
);
A useEffect
call which immediately calls a setState
function is almost always an example of state that should be derived.
It doesn't seem like the worst thing in the world, and on it's own, it probably isn't. That being said, if this pattern exists, there's a good chance it exists in many places and can lead to a larger problem.
useEffect hell
Most of us have been on a project that has gone through useEffect hell. Trying to fix that one bug but being unable to trace it because a single state change causes a flurry of new renders.
The thing with useEffect
is it can cause a cascading number of state updates... which in turn, can cause subsequent useEffect
calls. This isn't an issue with the function itself - it's an issue with excessive state.
Tips for managing state
If I had one piece of advice for managing state, it would be to keep it to a minimum... but I don't have one just one piece of advice - so here's some more!
Batch state updates
When multiple state updates are being called at one time, it's useful to batch these together into one call.
With batching
const [{ fetching, data }, setState] = useState({
fetching: true,
data: undefined
});
useEffect(() => {
(async () => {
const data = await getData();
setState({ fetching: false, data })
})()
}, []);
// State 1: { fetching: true, data: undefined }
// State 2: { fetching: false, data: 1234 }
Without batching
const [fetching, setFetching] = useState(true);
const [data, setData] = useState();
useEffect(() => {
(async () => {
const data = await getData();
setFetching(false);
setData(data);
})()
}, []);
// State 1: { fetching: true, data: undefined }
// State 2: { fetching: false, data: undefined }
// State 3: { fetching: false, data: 1234 }
Batched updates don't just mean fewer renders, there will be fewer possible states to deal with; making testing and reproductions much simpler.
Remember, fewer renders means fewer state changes
Use fixtures
Fixtures (or stories) are an incredible tool for understanding, modelling, and documenting all the states of your app.
Find out more about fixtures over here.
Try useMemo more often
It's surprising how much of an impact it can make.
Hopefully, you found this interesting! If you have any thoughts or comments, feel free to drop them below or hit me up on twitter - @andyrichardsonn
Disclaimer: All thoughts and opinions expressed in this article are my own.
Top comments (0)