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.

react.devuseEffect

Notes

The dependency array controls when the effect runs:

FormRuns
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 .current doesn'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 useEffect needs a correct dependency array.

Use case

Re-fetching a user's data whenever userId changes.

Code compare

React
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:

Angular
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, []) to ngOnInit. That's a fair analogy for setup, with two caveats: the effect runs after the first render (closer to ngAfterViewInit timing), and in development StrictMode runs 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

ReactAngular
useEffect(fn, [a, b])effect(() => { a(); b(); })
useEffect(fn, [])ngOnInit / constructor / afterNextRender
exhaustive-deps lint ruleAutomatic tracking
—untracked() to skip a dependency