DEV Community

Mateusz Kwiecień
Mateusz Kwiecień

Posted on

Readundant states

Working on multiple React projects, the most common issue I encounter is unnecessary states.

Such 'bad' states have a negative impact primarily on the quality of the code as well as its maintenance and development. They can also adversely affect the performance of the application or cause surprising bugs that are difficult to diagnose.

In this post, I will try to show an example of redundant state and discuss the potential problems it causes, as well as how to quickly and safely refactor it.

Problem

The example will be based on a real case. I will try to maintain a relatively original but simplified code structure. Surely, in many places, it would require significantly more refactoring. I do this intentionally because in real projects, there isn't always the possibility to spend days or weeks refactoring the entire code. Sometimes, we have to settle for just quick wins.

function useUserLoader() {
  const { data: users } = useQuery({
    queryKey: ["userList"],
    queryFn: () => getUserList(),
    placeholderData: [],
  });

  return { users };
}

function useUsersOptionLoader() {
  const [userOptions, setUserOptions] = useState([]);
  const { users } = useUserLoader();


  useEffect(() => {
    if (users) {
      setUserOptions(users.map(mapUserToOption));
    }
  }, [users]);

  return userOptions;
}
Enter fullscreen mode Exit fullscreen mode

In this case on the start we will have 4 renders. In case you don't believe me, as proof, I'm adding a screenshot from the profiler.

Screenshot from React Dev Tools showing the fourth renders that occurs

Let's go step by step and see what is happening in each render.

First render

  1. In this step, useUserLoader starts fetching data from the server and returns an empty list, which is defined in the placeholderData prop.
  2. useUsersOptionLoader initializes the userOptions state with an empty list.
  3. useEffect hook is called, and that hook updates the userOptions state with a new value, which is an empty array.
  4. useUsersOptionLoader returns the current state of userOptions, which is an empty array

Second render:

  1. This render occurs because userOptions state changed.
  2. In this step, useUserLoader is still fetching data from the server and returns an empty list.
  3. The useEffect hook is not called because the users value didn't change.
  4. useUsersOptionLoader returns the current state of userOptions, which is still an empty array.

Third render

  1. In this step, useUserLoader finishes fetching data from the server and returns the populated user list.
  2. The userOptions state is still an empty list.
  3. useEffect hook is called, and it updates the userOptions state with a new value, which is a mapped array of users.
  4. useUsersOptionLoader returns the current state of userOptions, which is still an empty array.

Fourth render

  1. In this step, useUserLoader returns the user list that was fetched in the previous render.
  2. The userOptions state is a mapped array of users, which was set in the previous render.
  3. The useEffect hook is not called because the users value didn't change.
  4. useUsersOptionLoader returns the current state of userOptions, which is a mapped array of users.

Key Takeaways:

  • Three renders return an empty array (first, second, and third), with the userOptions state getting updated only in the third render after the useEffect hook is called.
  • The fourth render reflects the updated userOptions state, which is now populated with the mapped array of users.

These 4 renders could potentially be a problem in large apps that render a lot of data. However, in most cases, this won't be a big deal.

The next issue with this code is that you need to spend additional time understanding what happens inside the useUsersOptionLoader. The combination of useEffect and useState introduces extra complexity. While this is a relatively simple hook, in more complex cases, you might encounter hooks with many lines of code, multiple states, and numerous dependencies. In such situations, you could easily spend hours analyzing and trying to understand the logic—and after making changes, you may end up breaking other parts of the code.

Solution

The solution to this kind of code is quite simple: remove the useEffect and useState, and just return the mapped value directly.

function useUsersOptionLoader() {
  const { users } = useUserLoader();
  return users.map(mapUserToOption);
}
Enter fullscreen mode Exit fullscreen mode

In this case on the start we will have 2 renders. In case you don't believe me, as proof, I'm adding a screenshot from the profiler.

Screenshot from React Dev Tools showing the two renders that occurs

Let's go step by step and see what happens in each render:

First render:

  1. useUserLoader starts fetching data from the server and returns an empty list, as defined in the placeholderData prop.
  2. useUsersOptionLoader returns an empty array.

Second render:

  1. useUserLoader finishes fetching data from the server and returns the populated user list.
  2. useUsersOptionLoader returns the mapped array of users.

Instead of the four renders you had previously, now there are only two, which are directly tied to fetching the data in useUserLoader. Additionally, the code is more concise with no extra useEffect or useState hooks. The useUsersOptionLoader code is now much clearer and more straightforward, making it easier to understand what’s happening.

Sumarize

With simple improvements in the code, it’s possible to optimize the application’s performance, reduce the code, and make it more readable. In the next article, I will show another example of redundant states and a simple solution to improve your app.

Top comments (0)