The most common problem I encounter when working on multiple React projects is unnecessary state.
This “bad” state primarily has a negative impact on code quality and its maintenance and development. They can also adversely affect an application’s performance or cause surprising errors that are difficult to diagnose.
In this article I will try to show an example of a redundant state and discuss the potential problems it causes and how to rebuild it quickly and safely.
question
This example will be based on a real case. I’ll try to keep the code structure relatively primitive but simplified. Of course, in many places this requires more refactoring. I do this intentionally because in real projects it’s not always possible to spend days or weeks refactoring the entire code. Sometimes we have to settle for a quick win.
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, initially we would have 4 Present. If you don’t believe me, as proof I’ll add a screenshot of the analyzer.
Let’s take a step-by-step look at what happens in each render.
first render
- In this step,
useUserLoader
Starts fetching data from the server and returns an empty list, which isplaceholderData
pillar. -
useUsersOptionLoader
initializationuserOptions
Status is an empty list. -
useEffect
Calls a hook that updates the userOptions state with a new value (an empty array). -
useUsersOptionLoader
Return to current statususerOptions
which is an empty array
Second rendering:
- This rendering occurs because
userOptions
The status has changed. - In this step,
useUserLoader
Still getting data from the server and returning an empty list. - this
useEffect
The hook is not called becauseusers
The value has not changed. -
useUsersOptionLoader
Return to current statususerOptions
which is still an empty array.
third rendering
- In this step,
useUserLoader
Completes retrieving data from the server and returning the filled user list. - this
userOptions
state is still an empty list. -
useEffect
The hook is called and the userOptions state is updated with the new value, which is the user’s mapped array. -
useUsersOptionLoader
Return to current statususerOptions
which is still an empty array.
fourth render
- In this step,
useUserLoader
Returns the list of users obtained in the previous render. - The userOptions state is a mapped array of users, set in a previous render.
- this
useEffect
The hook is not called because the user value has not changed. -
useUsersOptionLoader
Returns the current state of userOptions, which is a mapped array of users.
Key points:
- Three renders return an empty array (the first, second, and third), and the userOptions state is 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 users array.
In large applications that render large amounts of data, these 4 renderings can become a problem. However, in most cases, this isn’t a big deal.
The next problem with this code is that you need to spend extra time to understand what is happening inside useUsersOptionLoader. The combination of useEffect and useState introduces additional complexity. While this is a relatively simple hook, in more complex cases you may encounter hooks with multiple lines of code, multiple states, and numerous dependencies. In this case you can 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 simple: remove useEffect and useState and simply return the mapped value.
function useUsersOptionLoader() {
const { users } = useUserLoader();
return users.map(mapUserToOption);
}
In this case, initially we would have 2 Present. If you don’t believe me, as proof I’ll add a screenshot of the analyzer.
Let’s take a step-by-step look at what happens in each render:
First render:
-
useUserLoader
Starts retrieving data from the server and returns an empty list, as defined in the placeholderData attribute. -
useUsersOptionLoader
Returns an empty array.
Second rendering:
-
useUserLoader
Completes retrieving data from the server and returning the filled user list. -
useUsersOptionLoader
Returns an array of mapped users.
There are now only two renders, instead of the previous four, that are directly related to the material being retrieved useUserLoader
. In addition, the program code is more concise and has no redundant content. useEffect
or useState
hook. this useUsersOptionLoader
The code is now clearer and more direct, making it easier to understand what’s going on.
Summarize
By making simple improvements to your code, you can optimize your application’s performance, reduce your code, and make it more readable. In the next article, I will show another example of redundant state and a simple solution to improve the application.