The useState
and useEffect
hooks were a godsend for the React community. However, like any tool, these can easily be abused.
Here's one an example of one misuse I've seen a lot in my tenure as a software dev:
const MyAwesomeComponent = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState();
// ---- PROBLEMATIC HOOKS: ----
const [items, setItems] = useState([]);
const [itemsLength, setItemsLength] = useState(0);
useEffect(() => {
someAsyncApiCall().then(res => {
setData(res.data);
setLoading(false);
});
}, [setData, setLoading]);
// ---- UNNECESSARY USAGE OF HOOKS: ----
// anytime data changes, update the items & the itemsLength
useEffect(() => {
setItems(data.items);
setItemsLength(data.items.length || 0);
}, [data, setItems, setItemsLength]);
return (
// ...JSX
);
};
The problem with the above use case is that we are keeping track of some redundant state, specifically items
and itemsLength
. These pieces of data can instead be derived functionally from data
.
A Better Way:
Any data that can be derived from other data can be abstracted and re-written using pure functions.
This is actually pretty simple to pull off - here is one example:
const getItems = (data) => {
// I always like to protect against bad/unexpected data
if (!data || !data.items) return [];
return data.items;
};
const getItemsLength = (data) => {
return getItems(data).length;
};
Then, our component is simplified to the following:
const MyAwesomeComponent = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState();
// DERIVED DATA - no need to keep track using state:
const items = getItems(data);
const itemsLength = getItemsLength(data);
useEffect(() => {
someAsyncApiCall().then(res => {
setData(res.data);
setLoading(false);
});
}, [setData, setLoading]);
return (
// ...JSX
);
};
Takeaways
The cool thing about this pattern is that getItems
and getItemsLength
are very easy to write unit tests for, as the output will always be the same for a given input.
Perhaps the above example was a little contrived, but this is definitely a pattern I have seen in a lot of codebases over the years.
As apps scale, it's important to reduce complexity wherever we can in order to ward off technical debt.
tl;dr:
Using useState
and useEffect
hooks is often unavoidable, but if you can, abstract out any data that can be derived from other data using pure functions. The benefits can have huge payoffs down the road.
Banner Photo by Lautaro Andreani on Unsplash
Top comments (22)
The first approach is indeed wrong, because you should use useMemo for derived state, instead of useState+useEffect, the second approach is dangerous since it can lead to slow renders and infinite re-rendering, so if you are doing complex manipulation and/or passing the derived state down to another component you should use the derived state factories and place it inside a useMemo
If the calculations are expensive, then yes, useMemo would be ideal, however most derivations are quite trivial in my experience and using a pure function to compute takes less effort and cognitive load to set up. Also, pure functions are inherently easier to test as well. Thanks for the feedback!
You can declare the pure functions exactly the sema way, and test them as well, just wrap them inside the useMemo when calling, for me the golden rule is: if you are experienced, check your render times and render counts to decide using it or not, if you are a beginner and you are not sure always wrap derived state inside useMemo, because the using it when it is not needed will be negligible, but not using it when it is needed will give you so much headaches
I suppose "dangerous" only if those calculations are slow? if the time to execute them is negligible then useMemo would probably be overkill ... otherwise then yes, it would be the recommended approach :)
bro, the first code snippet has an error in it, I think:
Shoultd it not be this instead:
Good catch! Made the correction. Important to test first before sending off to QA 😅
setState in React is stable. You should remove them from the dependency array. I think array just like this [data]
I'm new with React and I don't understand why isn't this code triggering an infinite loop ? (we watch for changes in setData and setData is called within the useEffect)
useEffect(() => {
someAsyncApiCall().then(res => {
setData(res.data);
setLoading(false);
});
}, [setData, setLoading]);
UPDATE - see follow up below.
Your concern is 100% warranted - useEffect dependencies are often the cause of infinite loop headaches, mainly from objects, arrays, and function expressions.
The
set*
functions returned from theuseHook
call reference the same functions between renders. Or, in other words:The following as you pointed out would cause an infinite loop:
UPDATE - I think that React does some smart memoization for the dependencies, so my analysis is incorrect. The main thing that would cause a
useEffect
is performing asetState
(the same as using one of theset*
useState functions) inside of auseEffect
, where the state value is part of the dependency array.This is a good article that outlines some of the pitfalls and how to solve them: dmitripavlutin.com/react-useeffect...
Good article.👍
Though, there's one thing that I gotta mention. You don't need to include the state setters functions that are returned from useState in the useEffect dependency array, the reason being
that these function are guaranteed to have a stable signature between renders.
Good catch! I think the exhaustive-deps eslint rule used to complain if setter fncs were not included, so I just got into the habit if included everything in the dependency array, but you're absolutely correct (and eslint plugins have gotten a lot better over the years).
Thanks for this article
for me, the first approach is easier to reason about,
you see that items subscribe to changes of data,
but In the second approach, you can get messy
about why items are changing or when,
for the first approach using useMemo and
extracting logic to separate functions will make it better.
Thanks for the feedback. I'm not following how adding additional state and memoization helps to reason about the data. For me, simple data derivation is much easier to reason about, especially since there is less state being managed. As an app scales it especially becomes important to try and reduce complexity wherever possible.
Also second way will trigger multiple rerenders, so its not good way to handle it like that.
async/await (and decouple logic inside usecase)
Also use memo which Joa Lucas told to us its also good way to go
You can use optional chaining in your conditional checks now, instead of multiple || conditions
Old habits. :) I use optional chaining regularly in TS files, but didn't know it had such universal browser support now. Thanks for the tip!
developer.mozilla.org/en-US/docs/W...
Nice article, thanks
You should make a custom hook when dealing with any type of data...
Agree, I use custom hooks a lot - it's also important to not abstract things out too hastily. I'm a big fan of iterative refactoring.
Could you elaborate? Why not use swr/react-query?
Awesome