DEV Community

Cover image for You (probably) don't need that useState + useEffect
Don Juan Javier
Don Juan Javier

Posted on • Edited on

You (probably) don't need that useState + useEffect

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
  );
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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
  );
};
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
joaolss profile image
João Lucas Silva

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

Collapse
 
townofdon profile image
Don Juan Javier

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!

Collapse
 
joaolss profile image
João Lucas Silva

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

Collapse
 
leob profile image
leob

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 :)

Collapse
 
webkadabra profile image
WEBKADABRA

bro, the first code snippet has an error in it, I think:

useEffect(() => {
    setItems(data.items);
    setItems(data.items.length || 0);
  }, [data, setItems, setItemsLength]);
Enter fullscreen mode Exit fullscreen mode

Shoultd it not be this instead:

useEffect(() => {
    setItems(data.items);
    setItemsLength(data.items.length || 0);
  }, [data, setItems, setItemsLength]);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
townofdon profile image
Don Juan Javier

Good catch! Made the correction. Important to test first before sending off to QA 😅

Collapse
 
nvhung150196 profile image
nvhung150196

setState in React is stable. You should remove them from the dependency array. I think array just like this [data]

Collapse
 
devmeqa profile image
devmeqa

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]);

Collapse
 
townofdon profile image
Don Juan Javier • Edited

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 the useHook call reference the same functions between renders. Or, in other words:

[useState and useEffect] are guaranteed to have a stable signature between renders

The following as you pointed out would cause an infinite loop:

// object
const user = {};
useEffect(() => { /* ... */ }, [user]);

const dogs = [];
useEffect(() => { /* ... */ }, [dogs]);

const fetchSomethingCool = () => {};
useEffect(() => { /* ... */ }, [fetchSomethingCool]);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
townofdon profile image
Don Juan Javier

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 a setState (the same as using one of the set* useState functions) inside of a useEffect, 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...

Collapse
 
johnsawiris profile image
John Sawiris • Edited

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.

Collapse
 
townofdon profile image
Don Juan Javier

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).

Collapse
 
_faridjunior profile image
mohamed farid

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.

useMemo(() => {
    setItems(getItems(data));
    setItemsLength(getItemsLength(data));
  }, [data, setItems, setItemsLength]);

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;
};

Enter fullscreen mode Exit fullscreen mode
Collapse
 
townofdon profile image
Don Juan Javier

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.

Collapse
 
abukodonosor profile image
Abukodonosor

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

Collapse
 
schinta2 profile image
siva sandeep chintamaneni

You can use optional chaining in your conditional checks now, instead of multiple || conditions

Collapse
 
townofdon profile image
Don Juan Javier

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...

Collapse
 
sargalias profile image
Spyros Argalias

Nice article, thanks

Collapse
 
ebitzu profile image
eBitzu

You should make a custom hook when dealing with any type of data...

Collapse
 
townofdon profile image
Don Juan Javier

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.

Collapse
 
raulmarindev profile image
Raúl Marín • Edited

Could you elaborate? Why not use swr/react-query?

Collapse
 
rnz profile image
Rami al zayat

Awesome