Hooks
By the end of this lesson you'll know what hooks are, manage side effects with useEffect and its dependency array, reach into the DOM with useRef , share data with useContext , follow the Rules of Hooks, and package your own logic into a custom hook.
Part of the free React course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
🪝 So, what is a hook?
A hook is a special function whose name starts with use that lets a function component "hook into" React features — like remembering state or running side effects. Before hooks (2019), only clunky class components could do these things; hooks brought them to simple functions.
You already know one hook: useState . This lesson adds the rest of the everyday toolkit. They all share two rules you'll meet at the end: call them at the top level of your component, and only from React functions .
1. Recap: useState gives a component memory
A plain function forgets everything between calls. useState fixes that: it stores a value across renders and gives you a setter to change it. Calling the setter tells React "this value changed — re-render me." The return value is always a pair: [value, setValue] . This box is read-only JSX; read every comment.
2. useEffect — running side effects
A side effect is anything that reaches outside React to touch the wider world: fetching data, starting a timer, subscribing to an event, or writing to localStorage . useEffect runs that code after React paints the screen, so it never blocks rendering. The second argument — the dependency array — controls when it runs, and an optional returned function is the cleanup .
🔎 The three dependency-array patterns
useEffect(fn, []) — empty array. Runs once after the first render. Use for one-time setup (fetch on mount, add a global listener).
useEffect(fn, [a, b]) — runs after the first render and any time a or b changes value. Use to re-sync with changing props/state.
useEffect(fn) — no array. Runs after every render. Rarely what you want, and a common cause of infinite loops.
Cleanup: if your effect starts something (a timer, a subscription), return () => { ... } to stop it. React runs cleanup before the next effect and when the component unmounts.
Run this to see how the dependency array decides whether an effect re-runs — it uses the exact same Object.is comparison React does internally.
Your turn. The effect below should reconnect only when roomId changes, but the dependency array is empty. Fill in the blank so it watches the right variable, then run it and check the output.
3. useRef — DOM refs and mutable values
useRef hands you a small box, { current: ... } , that survives re-renders but, unlike state, does not cause one when you change it. Two everyday jobs: grab a real DOM node (to focus an input or scroll an element) via the ref= { ... } attribute, and remember a value between renders without re-rendering (like a timer id).
Run this to feel the key difference: a ref is just an object you can mutate in place, and mutating it never triggers a render — which is exactly why it's safe for a "count the renders" counter that state could never do without looping.
4. useContext — shared data without prop drilling
Passing a value down through many layers of components just to reach a deep child is called prop drilling , and it's tedious. useContext lets a deeply nested component read a value provided by an ancestor directly. It's perfect for things many components need: the current theme, the logged-in user, or the chosen language.
5. The Rules of Hooks
Hooks rely on being called in the same order on every render — that's how React keeps each one matched to its stored value. Break the order and React loses track. Two rules guarantee a stable order:
1. Only call hooks at the top level. Never inside loops, conditions ( if ), or nested functions. If you need conditional behaviour, put the condition inside the hook, not around it.
2. Only call hooks from React functions — from a component, or from another custom hook (a function whose name starts with use ). Never from a plain helper function or an event handler.
Tip: the eslint-plugin-react-hooks lint plugin catches both mistakes (and missing dependencies) automatically.
6. Writing a custom hook
When two components need the same stateful logic, you don't copy-paste it — you extract it into a custom hook : a function named use... that calls other hooks and returns whatever you want. This is React's main way to reuse logic. Run the worked useCounter below; we fake useState with a closure so the logic runs in plain JS.
Common Errors (and the fix)
- Infinite re-render loop: calling setState inside a useEffect that has no dependency array (or a dependency that changes every render). Each render triggers the effect, which sets state, which re-renders... Add a correct array, e.g. [] or [id] .
- Stale closure (effect uses old values): you left a value out of the dependency array, so the effect keeps using the value from when it was created. The fix React suggests: add every value the effect reads to the array.
- "React Hook useEffect has a missing dependency": the ESLint warning for the above. Add the named variable to the deps array (don't silence the warning blindly).
- "Rendered fewer/more hooks than during the previous render": you called a hook conditionally (inside an if or after an early return ). Move every hook to the top level.
- Timer or listener never stops: you forgot the cleanup. Return a function from the effect: return () => clearInterval(id) or removeEventListener(...) .
- "Invalid hook call": you called a hook from a normal function, an event handler, or outside a component. Hooks only run inside components or custom use... hooks.
Pro Tips
- 💡 Use the updater form when new state depends on old: setCount(c => c + 1) is safer than setCount(count + 1) inside effects and async code.
- 💡 One effect per concern. Don't cram a fetch and a timer into one useEffect — split them so each has its own deps and cleanup.
- 💡 Reach for useRef for anything that shouldn't trigger a render: timer ids, the previous value, or a DOM node.
- 💡 Custom hooks start with use — that's not a style choice; it's how the lint rules and React know to apply the Rules of Hooks.
📋 Quick Reference
Hook / pattern
What it does
useState(init)
Returns [value, setValue] ; setter re-renders
useEffect(fn, [])
Run once after first render (on mount)
useEffect(fn, [a])
Run on mount + whenever a changes
return () => {}
Cleanup: runs before re-run / on unmount
useRef(init)
Mutable .current ; no re-render on change
useContext(Ctx)
Read a value from a Provider ancestor
function useX()
Custom hook: reusable logic, calls other hooks
Frequently Asked Questions
Q: What's the difference between useState and useRef ?
Both persist a value across renders, but changing useState re-renders the component, while changing a useRef does not. Use state for anything shown on screen; use a ref for behind-the-scenes values like a timer id or a DOM node.
After React has rendered and painted the screen. With [] it runs once; with dependencies it runs again whenever one of them changes; with no array it runs after every render.
Almost always an effect that calls setState but has a missing or always-changing dependency array, so it re-runs forever. Give it the correct array — often [] or the specific values it reads.
Yes. The use prefix is how React and its lint rules recognise a hook and enforce the Rules of Hooks. useCounter works; getCounter would not be treated as a hook.
Mini-Challenge: build a useToggle hook
No blanks this time — just a brief and a starter outline. Write a useToggle custom hook that flips a boolean. The fakeUseState helper is provided so it runs in our editor; build it, run it, and check your output against the expected lines in the comments.
🎉 Lesson Complete
- ✅ A hook is a use... function that lets a component use React features
- ✅ useState gives memory; its setter re-renders the component
- ✅ useEffect runs side effects after render; the dependency array controls when
- ✅ [] = run once, [deps] = run on change, and return a function to clean up
- ✅ useRef stores mutable values and DOM nodes without re-rendering
- ✅ useContext shares data without prop drilling; follow the Rules of Hooks and bundle logic into custom hooks
- ✅ Next lesson: Forms and Controlled Components — capture and validate user input
Practice quiz
What is a React hook?
- A CSS class for styling components
- A type of class component
- A special function whose name starts with 'use' that lets a component use React features
- A way to import modules
Answer: A special function whose name starts with 'use' that lets a component use React features. Hooks are 'use...' functions that let function components hook into state, effects, and other React features.
What does useState return?
useState returns an array pair: the current value and a setter that triggers a re-render when called.
When does useEffect run relative to rendering?
- Before React renders
- Only on unmount
- During the render itself, synchronously
- After React renders and paints the screen
Answer: After React renders and paints the screen. useEffect runs after the render is committed and painted, so it never blocks rendering.
What does an empty dependency array, useEffect(fn, []), mean?
- Run after every render
- Run once after the first render (on mount)
- Never run
- Run only on unmount
Answer: Run once after the first render (on mount). An empty array has no dependencies to change, so after the first run the effect never re-runs.
What does useEffect with NO dependency array do?
- Runs after every render
- Runs once on mount
- Never runs
- Runs only when state changes
Answer: Runs after every render. With no array React cannot compare anything, so the effect runs after every render — a common cause of infinite loops.
How do you clean up a subscription or timer in useEffect?
- Call cleanup() at the end of the effect
- Use a separate useCleanup hook
- Return a function from the effect
- Set a cleanup flag in state
Answer: Return a function from the effect. The function you return from an effect is its cleanup; React runs it before the next effect and on unmount.
What is the key difference between useRef and useState?
- useRef can only hold DOM nodes
- Changing a useRef does NOT trigger a re-render; changing state does
- useRef triggers a re-render but useState does not
- There is no difference
Answer: Changing a useRef does NOT trigger a re-render; changing state does. Both persist across renders, but mutating ref.current never re-renders, while setting state does.
Which is the FIRST Rule of Hooks?
- Only call hooks inside loops
- Always call hooks inside an if statement
- Call hooks only in event handlers
- Only call hooks at the top level — never inside loops, conditions, or nested functions
Answer: Only call hooks at the top level — never inside loops, conditions, or nested functions. Hooks must be called at the top level so they run in the same order every render, keeping each matched to its stored value.
From where may you call a hook?
- Any plain JavaScript function
- Only from a React component or another custom hook
- Only from event handlers
- Only from the top of a file
Answer: Only from a React component or another custom hook. Hooks may only be called from React function components or from custom hooks (functions named use...).
What does useContext do?
- Creates a new component
- Replaces useState entirely
- Reads a value from a Provider ancestor without prop drilling
- Caches network requests
Answer: Reads a value from a Provider ancestor without prop drilling. useContext lets a deeply nested component read a value provided by an ancestor directly, avoiding prop drilling.
Continue this course
- Previous: Handling Events
- Next: Forms and Controlled Components