Functional components are taking center stage in React development, thanks partly to the streamlined useEffect Hook. It handles tasks like data fetching and local storage much more elegantly than lifecycle methods in class components. But there's a hidden shortcoming. useEffect can fire twice unexpectedly, potentially harming your app's performance. This blog post explores this challenge and provides clear solutions for effectively managing side effects using useEffect in React applications.
What is useEffect?
The useEffect Hook lets you do extra tasks in functional components clearly and predictably. It's like a replacement for the old lifecycle methods, but it puts all the extra task codes into one place, making the code easier to read and maintain.
However, there's a little hiccup with useEffect. Sometimes, when we use it, it runs twice when the component first shows up, which is a bit unexpected.
Let's explore why that happens and how to fix useEffect's double invocation.
Example of useEffect in React
Consider the following example of using useEffect in a React functional component:
import React, { useState, useEffect } from "react"; const MyComponent = () => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async () => { try { const response = await fetch("https://fakestoreapi.com/products/1"); const jsonData = await response.json(); setData(jsonData); setLoading(false); } catch (error) { console.error("Error fetching data:", error); setLoading(false); } }; fetchData(); }, []); return ( <div> {loading ? ( <p>Loading...</p> ) : ( <div> {data ? ( <div> <h2>Data from API:</h2> <pre>{JSON.stringify(data, null, 2)}</pre> </div> ) : ( <p>No data available</p> )} </div> )} </div> ); }; export default MyComponent;
In the example above, the user is trying to fetch data from an API. But if you check the network tab in the browser's developer tools, you'll notice that the API call happens twice. This duplication can slow down data loading, affecting the user experience.
By figuring out why it happens and learning how to solve it, you can ensure that the app only performs extra tasks when necessary. This will speed up loading times and give users a smoother experience.
Why does useEffect get called twice in React?
The first time useEffect runs is when the component appears on the screen. It's like doing tasks right after you see something new. This is important because it lets you get data or set up special features as soon as the component is ready.
The second time useEffect runs is when the component changes. This can happen if something about the component or its surroundings changes, like when its props or state changes or when a parent component updates. By default, useEffect runs every time the component changes, but you can control this by limiting it to run only when dependencies change.
Sometimes, it might seem like useEffect runs twice, especially when using React's Strict Mode, which helps catch problems during development by running components twice. Strict Mode ensures everything is working correctly by running components twice.
The solution: use the useMountEffect custom Hook
The useMountEffect custom Hook is a helper function designed to address an everyday use case for the useEffect Hook in React: running code only once when a component mounts.
How does the useMountEffect Hook work?
The useMountEffect Hook uses the useEffect Hook internally but with additional logic to check if it's the first render. If it is the first render, the Hook returns early, preventing the callback from executing.
The callback is executed on subsequent renders as expected, ensuring that the extra tasks occur only after the initial render.
UseMountEffect custom Hook code:
Let's start with a custom Hook that will help solve the double invocation issue.
// useMountEffect.js import { useEffect, useRef } from "react"; const useMountEffect = (callback, dependencies) => { const isFirstRender = useRef(true); useEffect(() => { if (isFirstRender.current) { isFirstRender.current = false; return; } return callback(); }, dependencies); }; export default useMountEffect;
Steps to implement the useMountEffect Hook
1. Import the useMountEffect Hook in the component where it needs to be used.
Import useMountEffect from "./path/to/useMountEffect";
2. Apply the useMountEffect Hook in your component.
import React, { useEffect } from 'react'; import useMountEffect from './path/to/useMountEffect'; const MyComponent = () => { // Your component logic // Use the useMountEffect Hook useMountEffect(() => { console.log('useEffect executed only once after mount'); }, []); // Inside an empty array you can pass your dependancies // More component logic }; export default MyComponent;
You can test the output by using useMountEffect in the MyComponent.jsx code.
import React, { useState, useEffect } from "react"; import useMountEffect from "./UseMountEffect"; const MyComponent = () => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useMountEffect(() => { const fetchData = async () => { try { const response = await fetch("https://fakestoreapi.com/products/1"); const jsonData = await response.json(); setData(jsonData); setLoading(false); } catch (error) { console.error("Error fetching data:", error); setLoading(false); } }; fetchData(); }, []); return ( <div> {loading ? ( <p>Loading...</p> ) : ( <div> {data ? ( <div> <h2>Data from API:</h2> <pre>{JSON.stringify(data, null, 2)}</pre> </div> ) : ( <p>No data available</p> )} </div> )} </div> ); }; export default MyComponent;
Conclusion
Leveraging custom Hooks like useMountEffect ensures side effects run only after the initial render, improving code clarity and preventing unintended behavior. This also translates to a performance boost by minimizing unnecessary re-renders, leading to a faster and more responsive user experience. Embrace custom Hooks for cleaner, more performant React applications. Experiment and explore!