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

townofdon

Don Juan Javier

Posted on November 1, 2021

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

💖 💪 🙅 🚩
townofdon
Don Juan Javier

Posted on November 1, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related