Skip to main content

Practice React Hooks

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 (opens in a new tab) (see the course page) 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; these courses go deeper on the same patterns.

CourseWhat it drillsAccessLength
Learn ReactuseState 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 freeFree15.1 hrs
What's 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 fileFree1.2 hrs
Advanced ReactReusability (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 ProPro18.8 hrs
React Interview QuestionsSpoken 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 ProPro41 min

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 (opens in a new tab)