Side Effects in Frontend Code
Identify I/O and mutations outside return values — where effects belong in UI apps, React rules, and how to test around them.
- javascript
- side-effects
A side effect is anything a function does besides returning a value from its inputs: writing the DOM, fetching, logging, timers, storage, global mutation. Frontend apps are made of effects — the skill is where they live and how you keep the rest pure enough to reason about.
Catalog of common effects
| Effect | Example |
|---|---|
| Network | fetch, WebSocket |
| Storage | localStorage, IndexedDB, cookies |
| DOM | el.focus(), imperative scroll |
| Scheduling | setTimeout, rAF |
| Randomness/time | Math.random, Date.now |
| Module state | push to a singleton cache |
function total(cart) {
analytics.track('total_computed'); // effect hidden in “pure” name
return cart.lines.reduce((s, l) => s + l.price, 0);
}
Structure that scales
UI event / framework lifecycle
↓
command / effect boundary (fetch, write storage)
↓
pure domain functions (pricing, validation, view-model map)
↓
setState / render
// pure
export function canCheckout(cart, inventory) {
return cart.items.every((i) => (inventory[i.sku] ?? 0) >= i.qty);
}
// effectful orchestration
export async function onCheckoutClick(cart, api) {
const inventory = await api.getInventory(); // effect
if (!canCheckout(cart, inventory)) {
return { ok: false, reason: 'stock' };
}
await api.placeOrder(cart); // effect
return { ok: true };
}
React mental model
- Render should be pure with respect to props/state.
- Event handlers are the right place for user-triggered effects.
useEffectsyncs with external systems after paint (subscriptions, imperative DOM).
useEffect(() => {
const id = setInterval(poll, 10_000);
return () => clearInterval(id); // cleanup is part of effect hygiene
}, []);
Putting fetch only in render without a gate is an effect in the wrong phase — Strict Mode double-invoke will expose it.
Why isolate effects
- Tests — pure functions need no MSW; orchestration tests mock
api. - Reuse — same pricing rules in web and native.
- Concurrency — pure calc can rerun; effects need idempotency/abort.
Idempotency and cleanup
useEffect(() => {
const ac = new AbortController();
load(id, { signal: ac.signal })
.then(setData)
.catch((e) => {
if (e.name !== 'AbortError') setErr(e);
});
return () => ac.abort();
}, [id]);
Effects that register listeners/timers without cleanup are SPA leaks.
Logging and analytics
Still effects. Prefer explicit calls at boundaries (track('purchase', payload)) over sprinkling inside reducers — reducers stay pure for time-travel/debug.
Interview answer (out loud)
“Side effects change something outside the return value — network, DOM, storage, time. I keep transforms pure and push effects to handlers, effects hooks, or service modules. Cleanup and abort matter for subscriptions and fetch. Hidden effects inside helpers make testing and Strict Mode painful.”
Module load effects
// module side effect on import — hard to test, order-sensitive
analytics.init();
Prefer explicit init() called from the app entry so tests can import modules without launching telemetry. Same rule for registering window listeners at module top level.
Further reading
Related
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.