You (probably) don't need that useState + useEffect
Don Juan Javier
Posted on November 1, 2021
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
Posted on November 1, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.