useEffect — React Hook

AkashSDas
4 min readMar 27, 2024

--

useEffect allow us to perform side effects in functional components. Side effects are data fetching, subscriptions, manual DOM manipulation, etc…

useEffect Hook

useEffect allow us to connect with non-React systems too like window, 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 the cleanup function (if provided) with the old values, and then run the setup function with new values. After our component is removed from the DOM, React will run the cleanup 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 with useLayoutEffect.
  • 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 with useLayoutEffect.

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 since users 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.

--

--