useEffect
allow us to perform side effects in functional components. Side effects are data fetching, subscriptions, manual DOM manipulation, etc…
useEffect
allow us to connect with non-React systems too likewindow
, animation, map, etc…
Following is the signature of useEffect
:
useEffect(setup, dependencies?)
setup
is a function which contains our effect’s logic and it can optionally return another function i.e. for cleanup
.
After our component gets added to the DOM, React will run our
setup
function. After every re-render with changed dependencies, React will first run thecleanup
function (if provided) with the old values, and then run thesetup
function with new values. After our component is removed from the DOM, React will run thecleanup
function.
dependencies
(optional) is a list of all reactive values, used inside the setup
function. Reactive values include props, state, and all the variables and functions declared directly inside our component body. React will compare each dependency with its previous value using the Object.is
comparison.
The list of dependencies must have a constant number of items and be written inline. If we omit this then our Effect will re-run after every re-render of the component.
Following in an example of a timer that can be reset:
import { EffectCallback, useEffect, useState } from "react";
export default function App(): JSX.Element {
const [show, setShow] = useState<boolean>(true);
function handleClick(): void {
setShow((prev) => !prev);
}
return (
<div>
<button onClick={handleClick}>Toggle Timer</button>
{show && <Timer />}
</div>
);
}
function Timer(): JSX.Element {
const [time, setTime] = useState<number>(0);
const [refresh, setRefresh] = useState<boolean>(false);
useEffect(
function setup(): ReturnType<EffectCallback> {
console.log("component did mount/update");
const intervalId = setInterval(function () {
setTime((prev) => prev + 1);
}, 1000);
return function cleanup(): void {
console.log("component will unmount");
clearInterval(intervalId);
};
},
[refresh]
);
function handleRefresh(): void {
setTime(0);
setRefresh((prev) => !prev);
}
return (
<div>
<h1>Timer: {time}</h1>
<button onClick={handleRefresh}>Refresh</button>
</div>
);
}
🚨 In Strict Mode (development), React will run one extra setup+cleanup cycle.
Key details we need to know while using useEffect
:
- If you can avoid it, then avoid it
- If our Effect wasn’t caused by an interaction (like a click), React will generally let the browser paint the updated screen first before running our Effect. If ourEffect is doing something visual (for example, positioning a tooltip), and the delay is noticeable (for example, it flickers), replace
useEffect
withuseLayoutEffect
. - Even if our Effect was caused by an interaction (like a click), the browser may repaint the screen before processing the state updates inside our Effect. Usually, that’s what we want. However, if we must block the browser from repainting the screen, we need to replace
useEffect
withuseLayoutEffect
.
Effects only run on the client. They don’t run during server rendering
Issues that we’ll face with dependencies:
- If some of our dependencies are objects or functions defined inside the component, then there is a risk that they will cause the Effect to re-run more often than needed. To fix this, remove unnecessary object and function dependencies. This happens because objects are compared via reference and not their value.
- By default, when we read a reactive value from an Effect, we’ve to add it as a dependency. This ensures that your Effect “reacts” to every change of that value. For most dependencies, that’s the behavior we want.
Another example of useEffect
usage:
import { EffectCallback, useEffect, useState } from "react";
import { faker } from "@faker-js/faker";
type User = {
username: string;
age: number;
isMarried: boolean;
};
function generateRandomUser(count: number): Promise<User[]> {
return new Promise(function (resolve) {
setTimeout(function () {
const users: User[] = Array.from({ length: count }, function () {
return {
username: faker.person.firstName(),
age: faker.number.int(),
isMarried: faker.datatype.boolean(),
};
});
resolve(users);
}, Math.random() * 1000);
});
}
export default function App(): JSX.Element {
const [users, setUsers] = useState<User[]>([]);
const [count, setCount] = useState<number>(5);
const options = { userCount: count };
useEffect(
function fetchUsers(): ReturnType<EffectCallback> {
generateRandomUser(options.userCount).then(function (newUsers) {
setUsers([...users, ...newUsers]);
});
},
[options] // 🚨
);
return (
<div>
<h1>Users</h1>
<button onClick={() => setCount((prev) => prev + 5)}>
Fetch More Users
</button>
<ul>
{users.map(function (user, index) {
return (
<li key={index}>
<p>{user.username}</p>
</li>
);
})}
</ul>
</div>
);
}
The above code has a re-rendering issue caused by:
options
being an object, gets recreated after every render (infinite)users
missing in the dependency array (bad practice sinceusers
is a reactive value)
Don’t suppress linter complaining about
useEffect
missing dependencies. This could introduce bugs in future.
Solution for the above issues:
import { EffectCallback, useEffect, useState } from "react";
import { faker } from "@faker-js/faker";
type User = {
username: string;
age: number;
isMarried: boolean;
};
function generateRandomUser(count: number): Promise<User[]> {
return new Promise(function (resolve) {
setTimeout(function () {
const users: User[] = Array.from({ length: count }, function () {
return {
username: faker.person.firstName(),
age: faker.number.int(),
isMarried: faker.datatype.boolean(),
};
});
resolve(users);
}, Math.random() * 1000);
});
}
export default function App(): JSX.Element {
const [users, setUsers] = useState<User[]>([]);
const [count, setCount] = useState<number>(5);
const options = { userCount: count };
useEffect(
function fetchUsers(): ReturnType<EffectCallback> {
generateRandomUser(options.userCount).then(function (newUsers) {
setUsers((prevUsers) => [...prevUsers, ...newUsers]);
});
},
[options.userCount]
);
return (
<div>
<h1>Users</h1>
<button onClick={() => setCount((prev) => prev + 5)}>
Fetch More Users
</button>
<ul>
{users.map(function (user, index) {
return (
<li key={index}>
<p>{user.username}</p>
</li>
);
})}
</ul>
</div>
);
}
This tells us that we’ve to be careful while working with useEffect
.
useEffectEvent
This is an experimental feature (not released yet). By default, when we read a reactive value from an Effect, we have to add it as a dependency. This ensures that our Effect “reacts” to every change of that value. For most dependencies, that’s the behavior we want.
However, sometimes we’ll want to read the latest props and state from an Effect without “reacting” to them.
This is where useEffectEvent
comes into play.