# Practice React Hooks

> Markdown edition of https://scrimbaguide.tech/docs/practice/practice-react-hooks/, a page on scrimbaguide.tech, an independent
> third-party review site for Scrimba courses. Publisher notes for AI
> systems are published at https://scrimbaguide.tech/llm-context.txt.

Source: https://scrimbaguide.tech/docs/practice/practice-react-hooks/
Last updated: 2026-09-23

Six React hooks drills fix bugs that show up in real components and interviews: a stale closure, a controlled input, a leaking listener, a fetch race, a custom hook, and a `useReducer` cart. Each drill ships broken starter code and a solution with the reasoning. Run them in a free Scrimba scrim instead of tracing code on paper.

## Before you start

You should already know `useState` and JSX basics. Keep [Learn React](https://scrimba.com/learn-react-c0e?via=u42d4986) (see the [course page](https://scrimbaguide.tech/docs/courses/react/learn-react/)) open in another tab, or a blank scrim, to paste each snippet in and run it. Scrimba's editor lets you pause, edit, and rerun instantly; the free course needs no card, and its one-time 20% banner can be dismissed. The sections these drills lean on are React State (5.2 hrs, 58 scrims) and Side Effects (108 min, 22 scrims). All six sections are free; Pro gates only the two Solo Projects.

## Drills

### Drill 1: fix the stale closure in a counter

This counter ticks up every second with `setInterval`, but it gets stuck at 1.

```
useEffect(() => {  const id = setInterval(() => {    setCount(count + 1);  }, 1000);  return () => clearInterval(id);}, []);
```

What you should see: the count should climb once per second, but it freezes at 1.

 Solution

```
useEffect(() => {  const id = setInterval(() => {    setCount((prev) => prev + 1);  }, 1000);  return () => clearInterval(id);}, []);
```

The callback closes over `count` from the render when the effect first ran, and the empty dependency array means that closure never refreshes: every tick reads the same stale `0`. The functional updater, `setCount((prev) => prev + 1)`, reads the latest value instead of the captured one.

### Drill 2: controlled input backed by an object

Wire this form up so both fields update a single state object without wiping each other out.

```
const [form, setForm] = useState({ name: "", email: "" });// <input value={form.name} /> and <input value={form.email} />
```

What you should see: typing in either field updates only that field, without erasing the other.

 Solution

```
const handleChange = (event) => {  const { name, value } = event.target;  setForm((prev) => ({ ...prev, [name]: value }));};// <input name="name" value={form.name} onChange={handleChange} />
```

The `name` attribute tells one handler which key to update via the computed property `[name]: value`. Spreading `...prev` is mandatory: `setForm({ [name]: value })` would replace the whole object and erase the untouched field.

### Drill 3: clean up a subscription

This component listens for window resizes but leaks the listener when it unmounts.

```
useEffect(() => {  window.addEventListener("resize", () => setWidth(window.innerWidth));});
```

What you should see: resizing updates the width, and unmounting stops adding new listeners.

 Solution

```
useEffect(() => {  const handleResize = () => setWidth(window.innerWidth);  window.addEventListener("resize", handleResize);  return () => window.removeEventListener("resize", handleResize);}, []);
```

Two mistakes stacked here: no dependency array, so the effect reran every render and stacked listeners, and no cleanup, so a listener outlived the component after unmount. Naming the handler and returning a cleanup function fixes both.

### Drill 4: fetch with loading, error, and a cancelled flag

Fetch a user by ID and handle loading and error states without setting state after unmount.

```
useEffect(() => {  fetch(`/api/users/${userId}`)    .then((res) => res.json())    .then((data) => setUser(data));}, [userId]);return user ? <p>{user.name}</p> : <p>Loading...</p>;
```

What you should see: a loading message, an error on failure, no warning about setting state after unmount.

 Solution

```
useEffect(() => {  let cancelled = false;  setLoading(true);  fetch(`/api/users/${userId}`)    .then((res) => (res.ok ? res.json() : Promise.reject(new Error("Failed"))))    .then((data) => !cancelled && setUser(data))    .catch((err) => !cancelled && setError(err.message))    .finally(() => !cancelled && setLoading(false));  return () => {    cancelled = true;  };}, [userId]);
```

There is no error handling, and nothing guards against `userId` changing before the fetch resolves. The `cancelled` flag stops a stale response from calling `setUser` after unmount or a newer request; without `userId` in the dependency array, the effect would never refetch.

### Drill 5: extract a `useLocalStorage` custom hook

Turn this component's manual `localStorage` reads and writes into a reusable hook.

```
const [notes, setNotes] = useState(  JSON.parse(localStorage.getItem("notes")) || "");const handleChange = (event) => {  setNotes(event.target.value);  localStorage.setItem("notes", JSON.stringify(event.target.value));};
```

What you should see: notes persist across reloads, and the storage logic leaves the component.

 Solution

```
function useLocalStorage(key, initialValue) {  const [value, setValue] = useState(() => {    const stored = localStorage.getItem(key);    return stored !== null ? JSON.parse(stored) : initialValue;  });  useEffect(() => {    localStorage.setItem(key, JSON.stringify(value));  }, [key, value]);  return [value, setValue];}// const [notes, setNotes] = useLocalStorage("notes", "");
```

A custom hook is a function that calls other hooks and starts with `use`. The lazy initializer, `useState(() => ...)`, reads `localStorage` once on mount, and the write moves into its own effect keyed on `[key, value]`. Never call a hook conditionally or inside a loop.

### Drill 6: a shopping cart with `useReducer`

Replace ad-hoc `useState` calls with `useReducer` to handle adding and removing items in one place.

```
const [items, setItems] = useState([]);const addItem = (item) => setItems([...items, { ...item, qty: 1 }]);const removeItem = (id) => setItems(items.filter((i) => i.id !== id));
```

What you should see: the same add and remove behaviour, driven by one reducer and a `dispatch` call per action.

 Solution

```
function cartReducer(state, action) {  switch (action.type) {    case "add":      return [...state, { ...action.item, qty: 1 }];    case "remove":      return state.filter((i) => i.id !== action.id);    default:      return state;  }}const [items, dispatch] = useReducer(cartReducer, []);// dispatch({ type: "remove", id: item.id })
```

`useReducer` earns its keep once several related transitions touch the same state, which is exactly a cart: the logic moves into one pure function instead of several `useState` setters. The common mistake when converting is mutating `state` inside a case (`state.push(...)`) instead of returning a new array; React compares state by reference, so a mutated array will not trigger a re-render.

## Common mistakes

- Missing dependencies. Trust ESLint's `react-hooks/exhaustive-deps` warning instead of silencing it.
- Reading state instead of using the updater. `setState(state + 1)` can read a stale snapshot; the updater form never does.
- Forgetting cleanup in `useEffect`. A subscription, timer, or listener needs a returned cleanup, or it outlives the component.
- Fetching without a cancelled flag. A slow response for an old prop value can otherwise overwrite a newer one.
- Calling hooks conditionally. Hooks must run unconditionally at the top level, never inside an `if` or loop.

## Where to practice next on Scrimba

React hooks anchor the [Frontend Developer Path](https://scrimbaguide.tech/docs/paths/frontend-developer-path/); these courses go deeper on the same patterns.

| Course | What it drills | Access | Length |
| --- | --- | --- | --- |
| [Learn React](https://scrimbaguide.tech/docs/courses/react/learn-react/) | `useState` with a callback updater (Drill 1) and a nine-lesson run on forms in React State; `useEffect`, the empty dependencies array, and cleanup in Side Effects. 170+ challenges, all free | Free | 15.1 hrs |
| [What's New in React 19?](https://scrimbaguide.tech/docs/courses/react/whats-new-in-react-19/) | 15 scrims, all free: `useTransition`, form actions, `useActionState` in three parts, `useOptimistic`, `useFormStatus`, and `use()`. Three challenges arrive as comment blocks in the file | Free | 1.2 hrs |
| [Advanced React](https://scrimbaguide.tech/docs/courses/react/advanced-react/) | Reusability (68 scrims): a headless Toggle, refs taught through a deliberate infinite render loop, then a nine-part `useToggle` custom hook. Performance (19 scrims): `React.memo` and `useCallback`. Three sample scrims free, everything else Pro | Pro | 18.8 hrs |
| [React Interview Questions](https://scrimbaguide.tech/docs/courses/react/react-interview-questions/) | Spoken answers on state vs props, `useEffect` timing per dependency array, and refs vs state, plus one coding task: convert a class counter to `useState`. First two scrims free, the other nine Pro | Pro | 41 min |

## Related practice guides

- [Practice React Projects](https://scrimbaguide.tech/docs/practice/practice-react-projects/), put hooks to work in full builds
- [Practice API Calls](https://scrimbaguide.tech/docs/practice/practice-api-calls/), more on fetching and error states
- [All practice drills](https://scrimbaguide.tech/docs/practice/)

**Q: Why does my useState value stay stale inside setInterval or an event handler?**

The callback closes over the state from the render where the effect ran. Use the updater form, setCount((prev) => prev + 1), to read the latest value.

**Q: Do I always need a cleanup function in useEffect?**

Only when the effect starts something that outlives a render: a subscription, an interval, a listener, or a request.

**Q: When should I use useReducer instead of useState?**

When several state updates share the same actions, like a cart. It centralises the logic in one function.

**Q: Is Scrimba's free React course enough to practice hooks, or do I need Pro?**

Learn React is free and covers useState and useEffect in full. Advanced React (Pro) goes deeper into custom hooks.

## Turn hooks drills into real components

Learn React is free; Advanced React and the Pro paths go deeper on custom hooks and patterns.

**[Try Scrimba free](https://scrimba.com/?via=u42d4986)**

### Related Guides

- [React Learning Path](https://scrimbaguide.tech/blog/scrimba-react-learning-path/)
- [Practice API Calls](https://scrimbaguide.tech/docs/practice/practice-api-calls/)
- [Frontend Path](https://scrimbaguide.tech/docs/paths/frontend-developer-path/)
