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 useMemo only 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 lazy useState initializer.

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 localStorage and saving new ones when the user picks a place.

Code compare

React
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} />;}
Angular
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 useMemo for derived state. The simpler default is to compute it directly in the render body; useMemo is 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

NeedReactAngular
Derived valueCompute in render (useMemo if expensive)computed()
Respond to a user actionEvent handlerMethod called from the template
Initial value from storageuseState(() => ...)signal(...) initializer