Handling Side Effects & Working with the useEffect() HookEntry 3 of 7
When You Don't Need an Effect
Code that looks like a side effect but belongs in an event handler, a state initializer, or the render body.
Notes
Effects are for synchronizing with systems outside React. They're often overused. Skip the effect when:
- The value can be derived. Compute it in the render body from props and state. Use
useMemoonly if the calculation is expensive. - The code responds to a user action. Put it in the event handler.
- The code is synchronous and needed for the first render. Reading
localStorage, for example, can happen in a lazyuseStateinitializer.
An effect that only sets state from other state is a warning sign: it causes an extra render and can loop.
What / Why / When
- What
Alternatives to
useEffect.- Why
Fewer effects mean fewer renders, fewer bugs, and simpler code.
- When
Before writing any effect, ask: "Is this syncing with something outside React?" If not, don't use one.
- Use case
Loading saved place IDs from
localStorageand saving new ones when the user picks a place.
Code compare
import { useState } from "react";const STORAGE_KEY = "selectedPlaces";export function PlacePicker() { // ✅ Synchronous read: lazy initializer, no effect needed const [pickedIds, setPickedIds] = useState<string[]>(() => JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]"), ); // ✅ Derived value: computed in the render body, no effect or extra state const pickedPlaces = AVAILABLE_PLACES.filter((place) => pickedIds.includes(place.id)); // ✅ Response to a user action: in the event handler function handleSelectPlace(id: string) { if (pickedIds.includes(id)) return; const next = [id, ...pickedIds]; setPickedIds(next); localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); } return <PlacesList places={pickedPlaces} onSelect={handleSelectPlace} />;}import { Component, computed, effect, signal } from "@angular/core";const STORAGE_KEY = "selectedPlaces";@Component({ selector: "app-place-picker", template: `<app-places-list [places]="pickedPlaces()" (select)="selectPlace($event)" />`,})export class PlacePickerComponent { pickedIds = signal<string[]>(JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]")); pickedPlaces = computed(() => AVAILABLE_PLACES.filter((place) => this.pickedIds().includes(place.id)), ); constructor() { // Keeping localStorage in sync with a signal is a legitimate effect in Angular effect(() => localStorage.setItem(STORAGE_KEY, JSON.stringify(this.pickedIds()))); } selectPlace(id: string) { this.pickedIds.update((ids) => (ids.includes(id) ? ids : [id, ...ids])); }}Differences and gotchas
- Your notes said to use
useMemofor derived state. The simpler default is to compute it directly in the render body;useMemois only an optimization. - Angular's
computed()is always the answer for derived values, and it's cached automatically. - Syncing a signal to storage with
effect()is idiomatic in Angular. In React, writing in the event handler is usually cleaner than an effect that watches state.
Quick reference
| Need | React | Angular |
|---|---|---|
| Derived value | Compute in render (useMemo if expensive) | computed() |
| Respond to a user action | Event handler | Method called from the template |
| Initial value from storage | useState(() => ...) | signal(...) initializer |