Course outline

React / Coordinated application behavior / Lesson 7

Effects and external synchronization

Use Effects to connect committed UI state to external systems, with complete dependencies and symmetric cleanup.

About 4 min readContent reviewed
On this page

Lesson goals

  • Ask whether an Effect is needed
  • Mirror subscription setup and cleanup
  • Test lifecycle transitions explicitly

Ask whether an Effect is needed

An Effect synchronizes a component with something outside React’s render calculation, such as a browser subscription or imperative widget. Derived display data belongs in render, and a user-triggered action belongs in its event handler. Moving every calculation into an Effect creates extra renders and synchronization obligations. State the external relationship in one sentence before writing the Hook: while this component is present, observe this browser event using these inputs. If no external system exists, reconsider whether the value can be calculated directly or whether the transition belongs to the event that caused it.

Dependencies describe reactive reads

An Effect’s dependency list includes the reactive values its setup reads, under the Hook’s rules. React compares dependencies with Object.is. The list is not a manually chosen schedule of preferred reruns. Omitting a read can capture stale inputs, while adding a newly created object can trigger setup repeatedly. Change the structure when a dependency is unnecessary: create a setup-only object inside the Effect or move a constant outside the component. Do not silence the dependency linter to force a desired timing. Cleanup runs with the old render’s captured values before a replacement setup and after unmount.

Mirror subscription setup and cleanup

The first example observes the browser’s online and offline events. Its setup registers one listener for each event and its cleanup removes those exact listener functions. The initial state reads navigator lazily because this is a client-host demonstration; a server-rendered design would need an explicit deterministic initial strategy. No real network request is made. Development Strict Mode can run an extra setup-cleanup cycle to test symmetry, so the code must behave correctly after reconnection. The test can dispatch synthetic browser events and verify the status rather than depending on a real service outage or an arbitrary delay.

jsx Component module — requires the course React host
import { useEffect, useState } from "react";
export default function App() { const [online, setOnline] = useState(() => navigator.onLine); useEffect(() => { const yes = () => setOnline(true); const no = () => setOnline(false); window.addEventListener("online", yes); window.addEventListener("offline", no); return () => { window.removeEventListener("online", yes); window.removeEventListener("offline", no); }; }, []); return <main><h1>Browser status</h1><p>{online ? "Online" : "Offline"}</p></main>; }
Expected output
After synthetic offline: Offline. After synthetic online: Online.

The Effect owns event listeners, not a network request. Synthetic offline and online events exercise state and cleanup in isolation.

Update external presentation after commit

The second example synchronizes document.title with a counter and restores the previous title when that particular setup is cleaned up. The dependency count reflects the value actually used, and cleanup captures the previous title from that setup. This is suitable for one isolated owner in the teaching host; a full application with nested routes should coordinate title ownership rather than let unrelated components overwrite one another. Effects run on the client, not during server rendering. They also are not a universal “after paint” guarantee for every interaction, so avoid depending on a precise visual timing that the API does not promise.

jsx Component module — requires the course React host
import { useEffect, useState } from "react";
export default function App() { const [count, setCount] = useState(0); useEffect(() => { const previous = document.title; document.title = `Count ${count}`; return () => { document.title = previous; }; }, [count]); return <main><h1>Title owner</h1><button type="button" onClick={() => setCount(c => c + 1)}>Increment</button><p>{count}</p></main>; }
Expected output
Initial document title: Count 0. After Increment: Count 1.

Each committed counter value becomes the title under one isolated owner. Cleanup restores the value that setup replaced.

Test lifecycle transitions explicitly

Verify mount, changed dependencies, unmount, and development setup-cleanup-setup behavior. A listener that seems harmless on the first mount can duplicate work after remounting if cleanup is missing. Async work requires an additional stale-result or cancellation policy because cleanup alone does not magically undo completed side effects. Keep unrelated synchronization concerns in separate Effects so each has a clear contract. If an Effect updates state and that state changes one of its dependencies, trace the cycle before adding memoization. The right repair may be removing redundant derived state rather than suppressing reruns.

Common errors and fixes

An Effect reads an old prop

Why it happens
Its dependency contract omits a reactive read.
How to fix it
Restore complete dependencies and restructure if necessary.
Prevent it next time
Keep the Hooks dependency linter enabled.

Listeners duplicate after remounting

Why it happens
Setup has no matching cleanup.
How to fix it
Remove the exact registered listener during cleanup.
Prevent it next time
Test setup-cleanup-setup.

A derived total causes extra renders and loops

Why it happens
An Effect maintains redundant state.
How to fix it
Calculate the total during render.
Prevent it next time
Ask which external system requires each Effect.

Check your understanding

1. Should filtering a local array by a query require an Effect?

Show worked solution

Usually no. Compute the filtered collection during render from the current array and query. An Effect that sets a second filtered-state value adds a stale intermediate render and a synchronization obligation without connecting to an external system. Measure before memoizing an expensive pure filter.

2. Why is the extra development Effect cycle useful for a subscription?

Show worked solution

It exposes cleanup that does not reverse setup, such as a listener left registered. A correct implementation can reconnect without duplicate work. Fix the lifecycle contract rather than hiding the probe with a ref that prevents necessary setup after a genuine remount.

Key takeaways

  • Effects synchronize external systems.
  • Dependencies describe reads, not preferences.
  • Cleanup must mirror setup.

Official references