Hooks
Effects connect React to the outside world, and the dependency list says when to reconnect
Guess first
Pick an answer. You will see if you were right at the end of the lesson.
What does the function returned from a useEffect callback do?
Real-world analogy
What is it?
Hooks are functions such as useState, useEffect and useRef that let a function component use React features. useEffect is the one for talking to things outside React. It runs after a render and re-runs when its dependencies change. The function it returns cleans up before the next run. Custom hooks combine these into reusable pieces, such as a debounced value or a data loader.
Real-world relevance
A search box that calls the API on every keystroke sends ten requests for ten letters. The slow ones can come back after the fast ones and show the wrong results. A debounce hook waits for a short pause, and the effect aborts the previous request when the query changes. A common review comment on effects is a missing dependency, which shows data for the wrong user after navigation.
Key points
- Rules of hooks — Call hooks at the top level of a component or custom hook, not inside loops, conditions or nested functions. React tracks hooks by call order, so the order must not change between renders.
- Effects sync with outside — useEffect is for things React does not control: network calls, timers, subscriptions, the document title. Do not use it for work you can do during render.
- The dependency array — List every reactive value the effect reads. React re-runs the effect when one of them changes, and skips it otherwise.
- Cleanup undoes the effect — Return a function that stops the timer, aborts the fetch or unsubscribes. React runs it before the next effect and when the component unmounts.
- Common effect mistakes — A missing dependency shows stale data. A fetch with no cleanup lets an old response overwrite a newer one. Setting state to a new object in an effect with no dependency array loops forever.
- useRef holds quiet values — A ref keeps a value between renders without causing a render when it changes. Use it for DOM nodes, timer ids or the previous value.
- useMemo and useCallback — They remember a result or a function between renders. Use them when a calculation is measured as slow. Also use them when a memoised child or a dependency list needs a stable identity.
- Custom hooks share logic — A function whose name starts with use can call other hooks. Each component that calls it gets its own separate state.
- Debounce waits for a pause — A useDebounce hook returns a value that only updates after the input stops changing. It saves a request per keystroke.
Quick check
Answer from memory — scrolling up first makes it stick less.
An effect reads userId but its dependency array is empty. What is the likely symptom?
Why can a hook not be called inside an if statement?
Code example
import { useEffect, useState } from 'react';
export function useDebounce<T>(value: T, delayMs: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delayMs);
return () => clearTimeout(timer);
}, [value, delayMs]);
return debounced;
}
export function SearchBox() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const [results, setResults] = useState<string[]>([]);
useEffect(() => {
if (!debouncedQuery) return;
const controller = new AbortController();
fetch('/api/search?q=' + encodeURIComponent(debouncedQuery), { signal: controller.signal })
.then(r => {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json() as Promise<string[]>;
})
.then(setResults)
.catch(e => { if (e.name !== 'AbortError') console.error(e); });
return () => controller.abort();
}, [debouncedQuery]);
return (
<div>
<input aria-label="Search" value={query} onChange={e => setQuery(e.target.value)} />
<ul>{results.map(r => <li key={r}>{r}</li>)}</ul>
</div>
);
}Line-by-line walkthrough
- 1. useDebounce is a custom hook. It keeps a copy of the value in its own state.
- 2. Its effect starts a timer. The cleanup clears the timer, so a fast new value cancels the old timer.
- 3. The dependency array lists value and delayMs, because the effect reads both.
- 4. SearchBox keeps the raw query for the input and the debounced query for the request.
- 5. The second effect runs only when debouncedQuery changes. It checks r.ok, ignores the expected AbortError, and its cleanup aborts the request that is no longer needed.
- 6. The input shows the raw query, so typing feels instant while the request waits for a pause.
Spot the bug
// assume: fetchName(id: number): Promise<string>, and useState/useEffect are imported
function Profile({ userId }: { userId: number }) {
const [name, setName] = useState('');
useEffect(() => {
fetchName(userId).then(setName);
}, []);
return <h1>{name}</h1>;
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Synchronizing with Effects (React)
- Reusing Logic with Custom Hooks (React)
- Free Full-Stack Playbook (React and TypeScript lessons) (Teamz Lab)
Lock it in