How to Skip React useEffect on Initial Render
Many React developers support using function components over class components. Function components are smaller, and offer the ability to build child UI elements that manage their own state. However, they do come with some drawbacks. One drawback that stood out to me on a recent bug-hunting session was the lack of control of initial render functionality for function components. This became apparent when I found an error caused because we were not bypassing the useEffect calls on the first render of a form page.
What was Our Issue with useEffect?
The useEffect hook allows React developers to queue actions based on a component’s state. React differs from predecessors like JQuery by allowing the building of stateful or stateless components. These components can keep track of user interactions and data through their internalized state variables.
The useEffect hook furthers the functionality of these stateful components. The main parts of useEffect are a callback function to perform an action and an array of state variables to watch for changes. When any of the state variables change, the callback kicks off:
This hook allows you to trigger actions from state mutations but has some limitations. The main limitation we’re concerned with is that the useEffect callback is called during the initial render, which may cause unintended side effects for your app.
Loading a Product’s Data from a Multi Select
To see this issue and a solution, imagine you are given a ticket to create a page allowing a user to select an item from a list and see all the item’s information. Users have a search bar to enter any query term on the initial page load. This query will fire off an API call to your server, returning a collection of search term adjacent items.
A common way to set the state of this page would be to make the search bar a controlled component (i.e. the parent page component maintains the state of this input and passes the query value back to the input for display) and have an onChange event handler on the input for setting the query term. To avoid unnecessary API calls you could add a debounce to the input to prevent making calls to the API every time a key is pressed. The query will be added to a useEffect hook that will trigger a call to the API for the products.
A summary of this process is: user enters a term in the text input, the input updates the query state, and the query state triggers the useEffect to get called sending a request to the API for data:
In theory, we have a good setup where we only get an API call whenever a user types in the text input. In practice, as we can see if we open our console, when the page first loads we will have an API call triggered with either an undefined or empty string for the query term depending on what our default value is for our query in useState. The useEffect is calling early and unnecessarily.
How to Use useRef to Bypass Initial Render useEffect Calls
It’s unclear from the docs, but when the component is loaded the state variables are initially set, likely causing the useEffect hook to trigger as the values are changed or defined. We need to be able to bypass this as this useEffect behavior for initial renders introduces unwanted side effects.
How should we do this? The best way I have found is with the useRef hook. This hook allows you to set and reference a value unrelated to the state. The last part of that statement is important as state changes affect other hooks and how and when components rerender.
We want to determine if the page has been initially rendered, so we know that useEffect is being called from a user interaction. To do this we can create a useRef variable for hasPageBeenRendered. We can then wrap our useEffect callback in a conditional wrapper referencing our useRef variable. A simple example would be as follows:
We import our React variables, initialize a useRef variable, initialize our query value with useState, and then set up our useEffect logic for calling the API. The initial state of the useRef is false, which causes us to bypass our API call before setting the useRef to true. We will enter the API call logic and query the database for subsequent calls.
Issues with this Approach
This works great as your browser and server consoles should now be devoid of extra calls and errors. However, this is a simple example. What would happen if we had multiple useEffect methods we wanted to prevent from prematurely triggering?
We could create a separate useRef variable for each useEffect. Here we would have one useRef for each useEffect we would like to prevent from being called on the initial render. This works but would lead to a cluttered component head as we get more complex logic.
Instead of adding a new useRef for each useEffect, why don’t we make our useRef value multi-dimensional? Currently, we have the useRef setting and updating a boolean value. To clean up our code, we can set the useRef value to a hash where every key refers to a different useEffect. It would look something like this:
This looks better as we can manage all of our logic from a central useRef variable. With this tweak, we can manage many useEffects without cluttering our function component.