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;
}
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.
Let's go step by step and see what is happening in each render.
First render
- In this step,
useUserLoader
starts fetching data from the server and returns an empty list, which is defined in theplaceholderData
prop. -
useUsersOptionLoader
initializes theuserOptions
state with an empty list. -
useEffect
hook is called, and that hook updates the userOptions state with a new value, which is an empty array. -
useUsersOptionLoader
returns the current state ofuserOptions
, which is an empty array
Second render:
- This render occurs because
userOptions
state changed. - In this step,
useUserLoader
is still fetching data from the server and returns an empty list. - The
useEffect
hook is not called because theusers
value didn't change. -
useUsersOptionLoader
returns the current state ofuserOptions
, which is still an empty array.
Third render
- In this step,
useUserLoader
finishes fetching data from the server and returns the populated user list. - The
userOptions
state is still an empty list. -
useEffect
hook is called, and it updates the userOptions state with a new value, which is a mapped array of users. -
useUsersOptionLoader
returns the current state ofuserOptions
, which is still an empty array.
Fourth render
- In this step,
useUserLoader
returns the user list that was fetched in the previous render. - The userOptions state is a mapped array of users, which was set in the previous render.
- The
useEffect
hook is not called because the users value didn't change. -
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);
}
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.
Let's go step by step and see what happens in each render:
First render:
-
useUserLoader
starts fetching data from the server and returns an empty list, as defined in the placeholderData prop. -
useUsersOptionLoader
returns an empty array.
Second render:
-
useUserLoader
finishes fetching data from the server and returns the populated user list. -
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)