Handling Side Effects & Working with the useEffect() HookEntry 2 of 7
useEffect and the Dependency Array
useEffect(setup, dependencies?) runs setup after the component renders, and re-runs it when a dependency changes.
Notes
The dependency array controls when the effect runs:
| Form | Runs |
|---|---|
useEffect(fn) | After every render |
useEffect(fn, []) | Once, after the first render |
useEffect(fn, [count, userId]) | After the first render, then whenever count or userId changes |
What goes in the array: every reactive value the effect reads, meaning props, state, and any variable or function declared in the component body that depends on them.
What stays out:
- Refs. The ref object never changes, and changing
.currentdoesn't trigger a render, so React can't react to it. - State setters and
dispatch. React guarantees they're stable. - Values declared outside the component (module-level constants, imported functions). They never change.
The eslint-plugin-react-hooks rule exhaustive-deps checks the array for you. Don't silence it; restructure the code instead.
Development note: In
<StrictMode>, React mounts, unmounts, and remounts every component once in development. An effect with[]therefore runs setup → cleanup → setup. This exposes missing cleanup and doesn't happen in production.
What / Why / When
- What
A declaration of which values the effect depends on.
- Why
Without it, the effect runs after every render; with the wrong values, it uses stale data.
- When
Every
useEffectneeds a correct dependency array.- Use case
Re-fetching a user's data whenever
userIdchanges.
Code compare
import { useEffect } from "react";export function UserActivity({ userId, count }: { userId: string; count: number }) { useEffect(() => { console.log(`Effect ran for user ${userId} with count ${count}`); }, [count, userId]); // re-runs only when count or userId changes return null;}effect() tracks every signal it reads, automatically. There's no dependency array to maintain:
import { Component, effect, input } from "@angular/core";@Component({ selector: "app-user-activity", template: "" })export class UserActivityComponent { userId = input.required<string>(); count = input(0); constructor() { effect(() => { console.log(`Effect ran for user ${this.userId()} with count ${this.count()}`); }); // re-runs when either signal changes }}To read a signal without making it a dependency, wrap the read in untracked().
Differences and gotchas
- Your note compared
useEffect(fn, [])tongOnInit. That's a fair analogy for setup, with two caveats: the effect runs after the first render (closer tongAfterViewInittiming), and in developmentStrictModeruns it twice. - React dependencies are listed by hand and compared with
Object.is. Angular dependencies are tracked from whichever signals the effect actually reads. effect()must be created in an injection context (see React Essentials, Entry 6).
Quick reference
| React | Angular |
|---|---|
useEffect(fn, [a, b]) | effect(() => { a(); b(); }) |
useEffect(fn, []) | ngOnInit / constructor / afterNextRender |
exhaustive-deps lint rule | Automatic tracking |
| — | untracked() to skip a dependency |