Lifecycle
| Since: | React 16.8(2019) |
|---|
In React, the lifecycle refers to the sequence of stages a component goes through: "mounting" when it first appears on screen, "updating" in response to data changes, and "unmounting" when it disappears. In function components, the useEffect hook is used to hook into each of these stages.
Main Lifecycle Phases
| Phase | Overview |
|---|---|
| Mount | The stage when the component is first added to the DOM. This is where initial data fetching or connecting to external services takes place. |
| Update | The stage when the component re-renders due to changes in state or props. Side-effect logic can be run in response to those changes. |
| Unmount | The stage when the component is removed from the DOM. Cleanup tasks such as clearing timers and removing event listeners are performed here. |
useEffect Syntax
import { useEffect } from 'react';
// Runs after every render (no dependency array)
useEffect(() => {
// side-effect logic
});
// Runs only on mount (empty dependency array)
useEffect(() => {
// side-effect logic
return () => {
// cleanup logic (runs on unmount)
};
}, []);
// Runs only when specified values change (dependency array provided)
useEffect(() => {
// side-effect logic
return () => {
// cleanup logic
};
}, [dependentValue]);
useEffect Second Argument (Dependency Array) Behavior
| Dependency Array | When It Runs |
|---|---|
| Omitted (no argument) | Runs after every render. Because this affects performance, specifying a dependency array is generally preferred. |
[] (empty array) | Runs once when the component mounts. Useful for initial data fetching. |
[value1, value2] | Runs when any value in the array changes compared to the previous render. Use this when you want to react to specific state or props changes. |
Sample Code
An example that performs processing at the mount, update, and unmount stages of a component.
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
// Runs every time isRunning changes
useEffect(() => {
// Do not run the timer when isRunning is false
if (!isRunning) {
return;
}
// Set an interval that increments seconds by 1 every second
const intervalId = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
// Cleanup function: called before the next effect runs or on unmount
// Clears the interval to prevent memory leaks
return () => {
clearInterval(intervalId);
};
}, [isRunning]); // Specify isRunning in the dependency array
return (
<div>
<p>Elapsed time: {seconds} seconds</p>
<button onClick={() => setIsRunning(true)}>Start</button>
<button onClick={() => setIsRunning(false)}>Stop</button>
<button onClick={() => { setIsRunning(false); setSeconds(0); }}>Reset</button>
</div>
);
}
export default Timer;
Sample Code: Data Fetching
An example that fetches data from an external API on mount.
import { useState, useEffect } from 'react';
function UserList() {
// Manage the user list, loading flag, and error information separately in state
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Use an empty dependency array to fetch data only on mount
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => {
// Throw an error if the response is not successful
if (!response.ok) {
throw new Error('Failed to fetch data.');
}
return response.json();
})
.then((data) => {
// Store the fetched data in state
setUsers(data);
setLoading(false);
})
.catch((err) => {
// Store the error information in state and end loading
setError(err.message);
setLoading(false);
});
}, []); // Empty array: runs only once on mount
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Error: {error}</p>;
}
return (
<ul>
{users.map((user) => (
// Always specify a unique key for list items
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default UserList;
Overview
Lifecycle control in function components is consolidated into the single useEffect hook. The roles of the three methods from the class component era — componentDidMount, componentDidUpdate, and componentWillUnmount — are expressed by varying how the dependency array is specified.
The cleanup function (the return () => { ... } part) is called just before the next effect runs and when the component unmounts. For resources that require explicit release — such as timers, event listeners, and WebSocket connections — cleanup must be performed in the cleanup function.
Multiple useEffect calls can be written in a single component. Splitting them by concern makes the code easier to follow.
Common Mistakes
Forgetting to add a value to the dependency array and referencing a stale value
If a variable is left out of useEffect's dependency array, the value captured inside the effect becomes stale (the closure trap). The ESLint rule react-hooks/exhaustive-deps can detect this automatically.
stale_ng.jsx
import { useState, useEffect } from 'react';
function Counter({ name }) {
const [count, setCount] = useState(0);
useEffect(function() {
const id = setInterval(function() {
// count is not in the dependency array, so it always references the initial value 0
console.log(name + ' count:', count);
}, 1000);
return function() { clearInterval(id); };
}, []); // count is missing
return <button onClick={function() { setCount(function(c) { return c + 1; }); }}>{count}</button>;
}
function App() {
return <Counter name="Kiryu Kazuma" />;
}
Fixed:
stale_ok.jsx
useEffect(function() {
const id = setInterval(function() {
console.log(name + ' count:', count);
}, 1000);
return function() { clearInterval(id); };
}, [count, name]); // List all values that are used
Forgetting to return a cleanup function and causing a memory leak
If a cleanup function is not returned from a useEffect that registers a timer or event listener, the resource continues to exist after the component unmounts, causing a memory leak. Always release registered resources in the cleanup function.
cleanup_ng.jsx
import { useEffect } from 'react';
function StatusMonitor({ userId }) {
useEffect(function() {
const intervalId = setInterval(function() {
console.log('Checking status for ' + userId);
}, 5000);
// No cleanup returned, so the timer keeps running after unmount
}, [userId]);
return <p>Monitoring: {userId}</p>;
}
Fixed:
cleanup_ok.jsx
import { useEffect } from 'react';
function StatusMonitor({ userId }) {
useEffect(function() {
const intervalId = setInterval(function() {
console.log('Checking status for ' + userId);
}, 5000);
// Clear the timer in the cleanup function
return function() { clearInterval(intervalId); };
}, [userId]);
return <p>Monitoring: {userId}</p>;
}
function App() {
return <StatusMonitor userId="Majima Goro" />;
}
Using an empty dependency array to run only on mount, but getting a state update error when async processing completes after unmount
When fetch is executed on mount with a [] dependency array, if the component unmounts before fetch completes, setState will be called afterward and produce an error. Logic to cancel the fetch in the cleanup function is required.
fetch_ng.jsx
import { useState, useEffect } from 'react';
function Profile({ userId }) {
const [data, setData] = useState(null);
useEffect(function() {
fetch('/api/users/' + userId)
.then(function(res) { return res.json(); })
.then(function(json) {
// Produces a warning if executed after unmount
setData(json);
});
}, []);
return <p>{data ? data.name : 'Loading...'}</p>;
}
Fixed:
fetch_ok.jsx
import { useState, useEffect } from 'react';
function Profile({ userId }) {
const [data, setData] = useState(null);
useEffect(function() {
let cancelled = false;
fetch('/api/users/' + userId)
.then(function(res) { return res.json(); })
.then(function(json) {
if (!cancelled) { setData(json); }
});
return function() { cancelled = true; };
}, [userId]);
return <p>{data ? data.name : 'Loading...'}</p>;
}
function App() {
return <Profile userId="spring_kasuga" />;
}
If you find any errors or copyright issues, please contact us.