# State & effects

> Make components interactive with useState, keep state minimal and immutable, and use useEffect only to sync with outside systems.

Frontend Developer roadmap · TypeScript & React · 17 min read · https://levelcode.org/roadmaps/frontend/react-state-and-effects

Props let a parent configure a component, but a component also needs to remember things of its own: what the user typed, whether a menu is open, which tab is selected. That is **state**. And sometimes a component needs to keep something outside React in sync, such as a timer, a browser API or a third-party widget. That is what **effects** are for. This lesson teaches both, and, just as importantly, how to recognise when you need neither.

## Why state exists

A local variable does not work for remembering things:

```tsx
function Counter() {
  let count = 0
  return <button onClick={() => count++}>Clicked {count} times</button>
}
```

Clicking changes `count`, but nothing re-renders, and if something else triggered a render, the function would run again and reset `count` to `0`. You need a value that survives between renders and that tells React to render again when it changes. That is `useState`:

```tsx
import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)
  return (
    <button type="button" onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  )
}
```

`useState(0)` returns the current value and a setter. Calling the setter schedules a re-render, and on that render `useState` returns the new value. Each instance of `<Counter />` on the page has its own independent state.

`useState` is a **Hook**. Hooks are functions starting with `use`, and they have two rules: call them only at the top level of a component or another Hook (never inside conditions, loops or nested functions), and only from React functions. React identifies each Hook by its call order, so that order must be the same on every render.

## State is a snapshot

Here is the most common surprise in React:

```tsx
function handleClick() {
  setCount(count + 1)
  setCount(count + 1)
  setCount(count + 1)
  console.log(count)   // still 0
}
```

After one click the counter shows **1**, not 3. During a render, `count` is a constant: the value React gave this render. All three calls compute `0 + 1`. The setter does not change the variable you already have; it asks React for a new render with a new value. React also **batches** updates, so all three are processed together in a single re-render after the handler finishes.

When the next state depends on the previous one, pass an **updater function**. React queues the updaters and runs each one with the latest pending value:

```tsx
function handleClick() {
  setCount((c) => c + 1)
  setCount((c) => c + 1)
  setCount((c) => c + 1)   // the counter now shows 3
}
```

React decides whether anything changed using `Object.is` on the old and new value. If you set the value it already has, React bails out without re-rendering the children.

## Updating objects and arrays

Because React compares by reference, you must **replace** objects and arrays in state, never mutate them:

```tsx
const [todos, setTodos] = useState<Todo[]>([])

// Wrong: same array, so React sees no change and skips the render
todos.push(newTodo)
setTodos(todos)

// Right: a new array
setTodos([...todos, newTodo])
```

| Goal | Immutable pattern |
| --- | --- |
| Add to an array | `[...items, item]` |
| Remove from an array | `items.filter((t) => t.id !== id)` |
| Update one item | `items.map((t) => (t.id === id ? { ...t, done: !t.done } : t))` |
| Change one object field | `{ ...form, email: value }` |
| Change a nested field | `{ ...user, address: { ...user.address, city } }` |

Avoid `push`, `splice`, `sort` and `reverse` directly on state arrays; copy first or use `toSorted()`, `toReversed()` and `with()`, which return new arrays.

## Where state should live

Good React code keeps state **minimal**: store the smallest set of values from which everything else can be calculated.

### Derive instead of storing

```tsx
// Redundant: fullName can drift out of sync with firstName and lastName
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [fullName, setFullName] = useState('')

// Better: compute it during rendering
const fullName = `${firstName} ${lastName}`.trim()
```

The same goes for filtered lists, counts, totals and validation results. If you can compute it from props or other state, compute it.

### Lift state up to share it

When two components need the same data, move the state to their closest common parent and pass it down as props, along with functions to change it:

```tsx
function FilterableList({ items }: { items: string[] }) {
  const [query, setQuery] = useState('')
  const visible = items.filter((item) => item.toLowerCase().includes(query.toLowerCase()))

  return (
    <>
      <SearchBox query={query} onQueryChange={setQuery} />
      <Results items={visible} />
    </>
  )
}

function SearchBox({ query, onQueryChange }: { query: string; onQueryChange: (q: string) => void }) {
  return (
    <input
      aria-label="Search"
      value={query}
      onChange={(e) => onQueryChange(e.target.value)}
    />
  )
}
```

The input is **controlled**: its value comes from state, and every keystroke updates that state. There is now a single source of truth for `query`.

### Reset state with a key

React keeps a component's state as long as the same component renders at the same position in the tree. To give it fresh state, for example when switching between user profiles, change its `key`: `<ProfileForm key={userId} userId={userId} />`. A new key means a new component instance.

## Effects: synchronising with the outside world

Some code has to run because a component is **on screen**, not because the user did something: connecting to a chat server, subscribing to a browser event, starting a timer. That is a side effect, and side effects cannot run during rendering, which must stay pure. `useEffect` runs your code **after** React has updated the DOM:

```tsx
import { useEffect, useState } from 'react'

export function OnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine)

  useEffect(() => {
    function update() {
      setOnline(navigator.onLine)
    }
    window.addEventListener('online', update)
    window.addEventListener('offline', update)

    return () => {
      window.removeEventListener('online', update)
      window.removeEventListener('offline', update)
    }
  }, [])

  return <p>{online ? 'Online' : 'Offline'}</p>
}
```

The function you return is the **cleanup**. React runs it before the effect runs again and when the component is removed. Every subscription, listener, timer or connection you start in an effect needs a matching cleanup, or it leaks.

### The dependency array

The second argument controls when the effect re-runs:

| Dependencies | Effect runs |
| --- | --- |
| omitted | after every render |
| `[]` | after the first render only (cleanup on unmount) |
| `[roomId, serverUrl]` | after the first render and whenever any listed value changed |

Every reactive value the effect reads (props, state and anything computed from them) must be listed. You do not choose dependencies; your code does. The `react-hooks/exhaustive-deps` lint rule, included in Vite's React template, checks this for you.

```tsx
function ChatRoom({ roomId }: { roomId: string }) {
  useEffect(() => {
    const connection = createConnection(roomId)
    connection.connect()
    return () => connection.disconnect()
  }, [roomId])
  // ...
}
```

When `roomId` changes, React disconnects from the old room (cleanup with the old `roomId`) and connects to the new one.

> [!NOTE]
> In development, `StrictMode` mounts every component, unmounts it and mounts it again, so your effect runs setup, cleanup, setup. This is deliberate: it proves your cleanup works. If the double run causes a bug, the fix is a correct cleanup, not removing `StrictMode`.

### Stale closures

An effect sees the values from the render that created it. This interval is broken:

```tsx
useEffect(() => {
  const id = setInterval(() => setSeconds(seconds + 1), 1000)
  return () => clearInterval(id)
}, [])   // seconds is always 0 inside, so the display sticks at 1
```

Listing `seconds` as a dependency would work but would recreate the interval every second. The cleaner fix is an updater, which needs no outside value: `setSeconds((s) => s + 1)`.

## When you need neither

Effects are an escape hatch for talking to systems outside React. Many effects in real codebases should not exist; they add an extra render, make data flow hard to follow and cause bugs. Before writing one, check this list:

- **Transforming data for display?** Calculate it during rendering. An effect that sets `filteredItems` whenever `items` changes is just a slower, buggier way to write `const filteredItems = items.filter(...)`. If the calculation is genuinely expensive, wrap it in `useMemo(() => ..., [items])`, or let the React Compiler memoise it for you.
- **Responding to a user action?** Put the code in the event handler. "Show a toast when the user clicks Buy" is about the click, not about the component being displayed.
- **Resetting state when a prop changes?** Use a `key`, as shown above.
- **Telling a parent about a change?** Call the parent's callback in the same event handler that changed the state.

```tsx
// Avoid: an effect reacting to state that an event set
useEffect(() => {
  if (submitted) showToast('Order placed')
}, [submitted])

// Prefer: do it where it happens
async function handleBuy() {
  await placeOrder(cart)
  showToast('Order placed')
}
```

> [!TIP]
> Ask "why does this code run?" If the answer is "because the user did X", it belongs in an event handler. If the answer is "because the component is visible and must stay in sync with Y", it is an effect.

Data fetching is a legitimate effect use, but doing it correctly by hand involves race conditions and caching. The next lesson covers that, and the library that does it for you.

## Practice

Build a `Stopwatch` component with:

1. A display of elapsed time in seconds with one decimal place, such as `12.3s`.
2. A Start/Stop button that toggles the stopwatch and a Reset button that sets it to zero.
3. The browser tab title showing the elapsed time while the stopwatch runs.
4. No leaked intervals: stopping, or removing the component, must clear the timer.

**Solution**

```tsx
import { useEffect, useState } from 'react'

export function Stopwatch() {
  const [running, setRunning] = useState(false)
  const [tenths, setTenths] = useState(0)

  // Sync with a browser timer while running
  useEffect(() => {
    if (!running) return
    const id = setInterval(() => setTenths((t) => t + 1), 100)
    return () => clearInterval(id)
  }, [running])

  // Derived during render, not stored in state
  const label = `${(tenths / 10).toFixed(1)}s`

  // Sync with the document title
  useEffect(() => {
    if (!running) return
    const previous = document.title
    document.title = label
    return () => {
      document.title = previous
    }
  }, [running, label])

  return (
    <div>
      <p>{label}</p>
      <button type="button" onClick={() => setRunning((r) => !r)}>
        {running ? 'Stop' : 'Start'}
      </button>
      <button
        type="button"
        onClick={() => {
          setRunning(false)
          setTenths(0)
        }}
      >
        Reset
      </button>
    </div>
  )
}
```

The interval effect depends only on `running`, so it is created once per start and cleared on stop or unmount. The updater `t => t + 1` avoids a stale closure. `label` is derived, so there is no state to keep in sync. Reset is a user action, so it lives in a click handler rather than an effect. For a production stopwatch you would store a start timestamp and compute elapsed time from `Date.now()`, since intervals can drift when a tab is in the background.

## Key takeaways

- `useState` gives a component memory and triggers a re-render when you set a new value.
- State is a snapshot per render; use updater functions when the next value depends on the previous one.
- Never mutate state: replace objects and arrays with new copies.
- Keep state minimal: derive values during rendering and lift shared state to the closest common parent.
- `useEffect` synchronises with systems outside React; list every dependency and always clean up.
- Event-driven logic goes in event handlers, not effects; most "derived state" effects should be deleted.

## Quiz

1. `count` is 0. A click handler calls `setCount(count + 1)` three times in a row. What does the component show after the click?

   A. 3
   B. 1
   C. 0
   D. It throws because setters cannot be called more than once

   **Answer: B.** `count` is a constant snapshot for that render, so each call sets 0 + 1. Use `setCount(c => c + 1)` to apply three increments.

2. A component calls `items.push(newItem); setItems(items)`. Why does the list not update?

   A. push is asynchronous
   B. setItems only accepts primitive values
   C. The array is the same reference, so React's Object.is check sees no change and skips the render
   D. React re-renders only on the second call

   **Answer: C.** React compares state by reference. Mutating the existing array and passing it back looks like 'no change'. Create a new array, for example `setItems([...items, newItem])`.

3. In development with StrictMode, a component with `useEffect(() => { console.log('setup'); return () => console.log('cleanup') }, [])` mounts. What is logged?

   A. setup, cleanup, setup
   B. setup
   C. setup, setup
   D. Nothing until the component unmounts

   **Answer: A.** StrictMode mounts, unmounts and remounts components in development to verify that effects clean up correctly. In production you would see a single 'setup'.

4. A component receives `price` and `quantity` props and must show their total. What is the best approach?

   A. Store total in state and update it in a useEffect that depends on price and quantity
   B. Store total in a ref
   C. Ask the parent to pass total as a third prop and keep it in state
   D. Compute `const total = price * quantity` during rendering

   **Answer: D.** Anything that can be calculated from props or state should be calculated during rendering. An effect would add an extra render and a moment where the total is out of date.
