Senior Frontend Depth
Hooks in Depth: Effects, Refs and the Rules
Effect dependencies, stale closures, useRef vs state, useLayoutEffect and the idea behind “You Might Not Need an Effect”.
On this page
- Why the Rules of Hooks exist
- useState
- useEffect: syncing with things outside React
- Stale closures
- useRef: a box you can change that does not cause a render
- useLayoutEffect
- More hooks you should know: useMemo, useCallback, useReducer and others
- "You might not need an effect"
- Custom hooks
- Scenario walkthroughs
- Common mistakes
- Interview questions
- Key takeaways
- What's next
In one line: a hook is a special React function (its name starts with use) that lets a function component keep state and stay in sync with the world outside React. Two ideas explain almost every hook bug. First, a closure (a function that remembers the variables around it) keeps the values from the render where it was created. This is called a stale closure when the values are old. Second, effects are for syncing with things outside React, not for reacting to your own state.
Four words appear all through this lesson. State is data that a component remembers between renders, and that changes what you see. A render is one call of your component function; it returns a description of the screen. A side effect is any work that reaches outside the component, such as a network call, a timer or a change to the page title. An effect is a block of code that React runs after a render, so you can do a side effect in a safe place.
You use hooks every day. Senior-level questions are about three things: why the rules exist, how old (stale) values appear, and how often an effect is the wrong tool.
Why the Rules of Hooks exist
There are two rules. Call hooks at the top level (not in loops, conditions or nested functions). Call them only from React functions (components or custom hooks).
Why? React does not know hooks by name. It keeps each component's hook state in an ordered list. On every render it matches each hook call to a place in the list by call order. Think of a row of numbered lockers: React finds your state by counting, not by reading labels.
function Form({ editing }: { editing: boolean }) { const [name, setName] = useState(""); // slot 0 if (editing) { const [draft, setDraft] = useState(""); // ❌ slot 1 only sometimes } const ref = useRef(null); // slot 1 or slot 2? React gets confused}If the number or order of hook calls changes between renders, React gives the wrong state to the wrong hook. The lint rule (react-hooks/rules-of-hooks) catches this. To fix it, put the condition inside the hook (useEffect(() => { if (editing) … })) or split the code into separate components.
useState
Definition: useState is a hook that gives a component a piece of state. It returns two things: the current value, and a function that sets a new value and asks React to render the component again with it.
Everyday comparison: a scoreboard. You read the number on it. To change it, you tell the scorer, who updates it and the crowd sees the new number.
const [count, setCount] = useState(0);const [user, setUser] = useState(() => readFromStorage()); // lazy initialiser: runs once, not on every rendersetCount((c) => c + 1); // updater form: use it when the new value depends on the old one- A state update replaces the value. Unlike class
setState, objects are not merged. WritesetUser({ ...user, name }). - Never change state directly (
user.name = "x"; setUser(user)). React compares by reference (is it the same object?). It sees the same object and does not re-render. - State is a snapshot for each render (Lesson 00). The setter does not change the variable you already hold; it schedules the next render.
- Passing the same value again (checked with
Object.is) makes React skip the re-render. React may still call your component once more before it stops, but it does not update the children or run effects.
useEffect: syncing with things outside React
Definition: useEffect is a hook that runs a function after React has drawn the component, so you can synchronise (keep in step) the component with something outside React. It can return a cleanup function. React calls the cleanup before the effect runs again and when the component is removed.
An effect runs after render. Its job is to keep something outside React in sync with your component. Examples are a subscription, a timer, a network request, a third-party widget or the document title.
useEffect(() => { const conn = createConnection(serverUrl, roomId); // setup: connect conn.connect(); return () => conn.disconnect(); // cleanup: disconnect}, [serverUrl, roomId]); // re-synchronise when these changeThink "keep in sync", not "run when X changes". The dependency array lists every reactive value the effect reads. Reactive values are props, state and anything worked out from them. When a dependency changes, React first runs the cleanup of the old effect. Then it runs the effect again with the new values.
[]: set up when the component first appears on screen (mount), clean up when it is removed (unmount).- No array: runs after every render. You rarely want this.
- Do not lie about dependencies. A lint rule is an automatic checker in your editor. React's
react-hooks/exhaustive-depsrule warns when the dependency array misses a value the effect reads. If you turn it off, you create stale closures. Fix the code instead. - Objects, arrays and functions created during render are new on every render. If you use them as dependencies, the effect runs again every time. Move them outside the component or into the effect. Or keep them stable with
useMemooruseCallback(explained below).
Strict Mode and effects
Strict Mode (<StrictMode>) is a React wrapper that runs extra checks in development only. For effects, after a component mounts, it runs one extra cycle of cleanup and setup. It does this to check that your setup and cleanup match each other. In production nothing extra happens. If you see doubled behaviour (two connections, two requests), your cleanup is incomplete (Debugging, Lesson 05).
Stale closures
A function remembers the variables from the render where it was created. An effect or handler created in render 1 keeps seeing the values of render 1. It does not see newer values.
Scenario: an interval keeps logging an old count.
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { console.log(count); // ❌ always logs 0: this callback closed over count from the first render setCount(count + 1); // ❌ always sets 1 }, 1000); return () => clearInterval(id); }, []); // empty deps: the effect (and its closure) never refreshes return <p>{count}</p>;}Here are the fixes, best first:
// 1) Functional update: no need to read count from the closureuseEffect(() => { const id = setInterval(() => setCount((c) => c + 1), 1000); return () => clearInterval(id);}, []);// 2) Honest dependencies: re-create the interval when count changes (fine for infrequent changes)useEffect(() => { const id = setInterval(() => console.log(count), 1000); return () => clearInterval(id);}, [count]);// 3) A ref holding the latest value (when the callback must stay stable)const latest = useRef(count);useEffect(() => { latest.current = count; });useEffect(() => { const id = setInterval(() => console.log(latest.current), 1000); return () => clearInterval(id);}, []);useEffectEvent: read the latest value without re-running the effect
Definition: useEffectEvent is a hook (stable since React 19.2) that wraps a function so it always sees the latest props and state, yet it is not a dependency of the effect. You call it only from inside effects (or from other effect events). Do not pass it to child components and do not put it in a dependency array. On older React versions, use a ref as in fix 3.
function Chat({ roomId, theme }: { roomId: string; theme: string }) { const onConnected = useEffectEvent(() => showToast("Connected", theme)); // always sees the latest theme useEffect(() => { const conn = createConnection(roomId); conn.on("connected", onConnected); // changing theme does not reconnect conn.connect(); return () => conn.disconnect(); }, [roomId]); // onConnected is not listed}The same snapshot rule explains "stale state in async handlers". A setTimeout or a fetch().then created in an earlier render sees the values of that earlier render.
useRef: a box you can change that does not cause a render
Definition: useRef is a hook that gives you a plain box, { current: ... }. The same box is returned on every render, and changing box.current does not cause a re-render. Its two common uses are holding a DOM element (such as an <input>) and holding a value that must survive renders but is not shown on screen.
const inputRef = useRef<HTMLInputElement>(null); // DOM accessconst timerId = useRef<number | undefined>(undefined); // any changing value that must survive rendersconst prevValue = useRef(value); // "previous value" patternuseState | useRef | |
|---|---|---|
| Changing it | Triggers a re-render | Does not trigger a re-render |
| Reading during render | Safe, expected | Avoid reading or writing ref.current during render (it is not pure) |
| Use for | Anything that changes what you see | DOM nodes, timers, "latest value" boxes, values that are not shown on screen |
If the screen should change when the value changes, use state, not a ref. A ref is an escape hatch (a way out of the normal React flow). In React 19, function components receive ref as a normal prop. So you do not need forwardRef in new code.
useLayoutEffect
Definition: useLayoutEffect is a hook that works like useEffect, but its function runs earlier: right after React changes the page structure (the DOM) and before the browser paints (draws pixels on screen). The difference in one line: useEffect runs after paint, useLayoutEffect runs before paint.
This runs after React changes the DOM but before the browser paints. Use it to measure the DOM and fix things right away, for example a tooltip position, so the user sees no flicker. It blocks painting, so keep it light. Effects do not run during server rendering. For code that needs the browser, prefer useEffect unless you really need to run before paint.
More hooks you should know: useMemo, useCallback, useReducer and others
These hooks are not needed in every component. Each one solves one clear problem. Use them when you have that problem, not before.
First, one word: memoization. To memoize (also spelled memoise) means to remember the result of work you already did, so you do not do the same work again. React has two hooks for this:
| Hook | What it remembers (memoizes) | Short definition |
|---|---|---|
useMemo | a value (a number, a string, an array, an object, any result) | "Remember this value until its inputs change." |
useCallback | a function | "Remember this function until its inputs change." |
Both hooks take two things: what to remember, and a list of inputs (called the dependency array). While the inputs stay the same, React gives you the saved thing back. When one input changes, React makes a new one and saves that instead.
useMemo: remember the result of a calculation
Definition: useMemo is a React hook that memoizes a value. It runs your function during render, saves the returned value, and gives you that same saved value on the next renders. It runs the function again only when something in the dependency array changes.
Everyday comparison: you work out a hard sum on paper and write the answer on a sticky note. Next time someone asks, you read the note. You only do the sum again if one of the numbers changes.
Use it when:
- The calculation is really slow (a big list to filter or sort).
- You need the same object or array each render, because it is a dependency of an effect or a prop of a memoised child.
Do not use it when the calculation is cheap (like first + " " + last). The memo itself costs a little time and makes the code harder to read. It is a speed hint, not a promise. React may throw the saved value away and calculate again, so your code must still be correct without it. (In development, Strict Mode also calls your function twice to check that it is pure.)
function ProductList({ items, query }: { items: Product[]; query: string }) { // Filtering 10,000 items is slow. Only redo it when items or query change. const visible = useMemo( () => items.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())), [items, query], ); return <ul>{visible.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;}useCallback: remember a function
Definition: useCallback is a React hook that memoizes a function. It saves the function itself (it does not run it) and gives you the same function on the next renders. It makes a new function only when something in the dependency array changes.
The difference in one line: useMemo remembers the result of calling a function. useCallback remembers the function, not its result.
Everyday comparison: you give your friend your phone number once. If you get a new number every day, your friend can never tell if "it is still you". Keeping the same number means "nothing changed".
Why does this matter? A function written inside a component is a new function on every render, even if the code is identical. A memoised child component (wrapped in React.memo) sees a new prop and renders again. An effect that lists the function as a dependency also runs again.
Use it when:
- You pass a function to a child wrapped in
React.memo. - The function is a dependency of
useEffectoruseMemo.
Do not use it when the function goes to a normal element like <button onClick={...}>, or to a child that is not memoised. Nothing gets faster, and you only add noise. In fact, useCallback(fn, deps) is the same as useMemo(() => fn, deps).
const Row = React.memo(function Row({ id, onSelect }: { id: string; onSelect: (id: string) => void }) { return <li onClick={() => onSelect(id)}>{id}</li>;});function List({ ids }: { ids: string[] }) { const [selected, setSelected] = useState<string | null>(null); // Same function every render, so memoised Row components do not re-render for nothing. const handleSelect = useCallback((id: string) => setSelected(id), []); return <ul>{ids.map((id) => <Row key={id} id={id} onSelect={handleSelect} />)}</ul>;}See Debugging, Lesson 03 for when memoisation really helps. If you use the React Compiler, it adds this kind of memoisation for you, so you write useMemo and useCallback by hand much less.
useReducer: one function that decides how state changes
Definition: useReducer is a hook that keeps state like useState, but it puts all the ways the state can change in one function. It returns the current state and a dispatch function. You call dispatch with an action (a small object that says what happened, such as "add item"). A reducer is a pure function that takes the old state and the action, and returns the new state. (Pure means it only uses its inputs and changes nothing else.)
Everyday comparison: a shop counter. Customers do not walk into the stockroom and change the numbers. They tell the clerk what they want ("buy one", "return one"). The clerk follows fixed rules and updates the books.
Use it when:
- One piece of state has many related parts that change together.
- The next state depends on rules (a cart, a form wizard, a game).
- You want to test the rules without rendering anything.
Do not use it when you only have one simple value, such as a toggle or a counter. useState is shorter. The dispatch function never changes between renders, so you can pass it down without useCallback.
type State = { items: string[] };type Action = { type: "add"; item: string } | { type: "clear" };function reducer(state: State, action: Action): State { switch (action.type) { case "add": return { items: [...state.items, action.item] }; // return a new object, never change the old one case "clear": return { items: [] }; }}function Cart() { const [state, dispatch] = useReducer(reducer, { items: [] }); return ( <> <button onClick={() => dispatch({ type: "add", item: "Tea" })}>Add tea</button> <button onClick={() => dispatch({ type: "clear" })}>Clear</button> <p>{state.items.length} items</p> </> );}useId: make a unique ID
Definition: useId is a hook that returns a unique text ID for your component. The ID is the same on the server and in the browser, and different for each place the component is used.
Everyday comparison: a cloakroom ticket. Each coat gets its own number, and the number matches on both halves of the ticket.
Use it when you connect two elements with an attribute, such as <label htmlFor> and <input id>, or aria-describedby. A fixed ID like "email" breaks when the component is used twice on one page. A random ID (Math.random()) breaks hydration (the step where React attaches to server-made HTML), because the server and the browser would make different values.
Do not use it for list key values. Also, do not rely on the shape of the string (it has changed between React versions: older versions made strings like :r0:, React 19 makes strings like _r_0_).
function EmailField() { const id = useId(); // a generated string such as "_r_0_". React makes sure it matches between the server and the client. return ( <> <label htmlFor={id}>Email</label> <input id={id} type="email" /> </> );}useSyncExternalStore: read data that lives outside React
Definition: useSyncExternalStore is a hook that lets a component read from a store that React does not own (a store is any place that keeps data, such as the browser or a state library), and re-render when that store changes. It also makes sure that every part of the screen shows the same value in one render.
Everyday comparison: a notice board in a hallway. You do not own it. Someone tells you when a new note is pinned, and then you go and read it.
Use it when:
- You read browser data that changes by itself (online status, window size,
matchMedia). - You write a state library, or you connect a store such as Redux or Zustand to React.
Do not use it for normal data your component owns. Use useState for that. You pass three things: how to subscribe, how to read the current value, and (for server rendering) what value to use on the server. The read function must return the same value if nothing changed.
function subscribe(callback: () => void) { window.addEventListener("resize", callback); return () => window.removeEventListener("resize", callback); // cleanup}function useWindowWidth() { return useSyncExternalStore( subscribe, () => window.innerWidth, // current value in the browser () => 1024, // value to use on the server );}use (React 19): read a promise or context while rendering
Definition: use is a React API (it looks like a hook) that reads a value from a resource while the component renders. The resource is either a promise (an object for a result that arrives later) or a context (a value shared with all components below a provider). If the promise is not finished yet, React shows the nearest Suspense fallback (the temporary screen from <Suspense fallback=...>, see Lesson 02) and waits.
Everyday comparison: you order food and get a buzzer. You sit down (the fallback) until the buzzer rings. Then you pick up your food and continue.
Use it when:
- A parent gives you a promise as a prop and you want to read it with Suspense.
- You want to read a context after an
ifor inside a loop. Unlike other hooks,usemay be called inside conditions.
Do not use it to create a new promise during render in a Client Component. React cannot keep the result of a new promise between renders, so it warns and the component can keep waiting. Create the promise in a Server Component, or use a cache or a data library. Also, use does not catch errors: if the promise rejects, the error goes to the nearest error boundary (a component that shows a fallback when its children throw), so add one. Do not wrap use in try/catch.
// Parent (a Server Component) starts the request and passes the promise down.function Page() { const commentsPromise = getComments(); return ( <Suspense fallback={<p>Loading comments…</p>}> <Comments commentsPromise={commentsPromise} /> </Suspense> );}// Child (a Client Component) reads the promise with use()."use client";import { use } from "react";function Comments({ commentsPromise }: { commentsPromise: Promise<string[]> }) { const comments = use(commentsPromise); // waits here, shows the fallback while waiting return <ul>{comments.map((c) => <li key={c}>{c}</li>)}</ul>;}(In a real project, Page and Comments live in two files, because "use client" must be at the top of its own file. They are shown together here to keep the example short.)
useContext: read a shared value
Definition: useContext is a hook that reads the value of a context. A context is a value (such as the theme or the logged-in user) that a parent shares with every component below it, so you do not pass it through props at each level. The difference from use: useContext must run at the top level, while use(Context) may be called inside an if.
const ThemeContext = createContext("light");function Button() { const theme = useContext(ThemeContext); // the nearest <ThemeContext value="..."> above, or "light" return <button className={theme}>OK</button>;}useTransition and useDeferredValue: keep typing smooth
useTransition is a hook that lets you mark a state update as not urgent (a transition). It returns isPending (true while the transition renders) and startTransition. React can interrupt a transition if the user does something more urgent. useDeferredValue(value) is a hook that returns a copy of value that is allowed to lag behind: first it keeps the old value, then it updates in the background. Difference in one line: useTransition wraps the update (you own the setter), useDeferredValue wraps the value (you only receive it, for example as a prop). Full explanation in Lesson 02.
useOptimistic: show the result before the server answers
Definition: useOptimistic is a hook (React 19) that shows a temporary, guessed version of state while an async action (such as saving) is still running. When the action ends, React drops the guess and shows the real state, whether the save worked or failed.
Everyday comparison: you send a chat message. It appears in the chat at once, in grey. If it fails, the grey message goes away.
function Todos({ todos, addTodo }: { todos: string[]; addTodo: (t: string) => Promise<void> }) { const [optimisticTodos, addOptimistic] = useOptimistic(todos, (current, text: string) => [...current, text]); async function action(formData: FormData) { const text = String(formData.get("text")); addOptimistic(text); // shows the new item immediately await addTodo(text); // the real list (todos) arrives after this } return ( <form action={action}> <input name="text" /> <ul>{optimisticTodos.map((t) => <li key={t}>{t}</li>)}</ul> </form> );}useActionState: state that is updated by an action
Definition: useActionState is a hook (React 19) that runs an action (an async function, often a form submit) and keeps its latest result as state. It returns [state, formAction, isPending]. You give it the action and an initial state; the action gets the previous state and the form data, and its return value becomes the new state.
async function signup(prev: { error: string | null }, formData: FormData) { const email = String(formData.get("email")); return email.includes("@") ? { error: null } : { error: "Enter a valid email" };}function SignupForm() { const [state, formAction, isPending] = useActionState(signup, { error: null }); return ( <form action={formAction}> <input name="email" /> <button disabled={isPending}>Sign up</button> {state.error && <p>{state.error}</p>} </form> );}After a form action finishes, React 19 clears the form's uncontrolled fields. Keep the typed values in state if you need them after an error.
Other hooks exist for rare jobs: useImperativeHandle (choose what a parent can call on a ref), useInsertionEffect (for CSS-in-JS libraries) and useDebugValue (a label in React DevTools). You will seldom write them yourself.
"You might not need an effect"
This is the most important modern lesson. Many effects are bugs waiting to happen. They add an extra render and make the data flow hard to follow. Try these alternatives first.
1. Work out values during render instead of syncing state
// ❌ Effect that copies props into state, causing an extra render and a flash of stale dataconst [fullName, setFullName] = useState("");useEffect(() => { setFullName(first + " " + last); }, [first, last]);// ✅ Just compute itconst fullName = first + " " + last; // cheapconst visible = useMemo(() => filter(items, q), [items, q]); // expensive: memoise2. Handle user events in event handlers
// ❌ A POST triggered by an effect reacting to state set by a clickuseEffect(() => { if (submitted) post(form); }, [submitted]);// ✅ The click is the cause: do the work in the handlerfunction handleSubmit() { post(form); }If code runs because the user did something, put it in the handler. Effects are for code that runs because the component was shown, or because a value it syncs with changed.
3. Reset state with a key, not an effect
// ❌ useEffect(() => { setComment(""); }, [userId]);// ✅<Profile key={userId} userId={userId} /> // fresh state per user4. Do not chain effects that set state to trigger other effects
A chain of effects (A sets state, B reacts, C reacts) causes many renders and breaks easily. Do all the work in one event handler or one reducer.
5. Data fetching
Fetching in an effect is allowed, because it syncs with a server. But you must clean up to avoid races (Debugging, Lesson 05). A race is when an old request finishes after a newer one and overwrites the newer result. A better choice is a data library (TanStack Query, SWR) or framework data loading (Server Components, loaders). They give you caching, no duplicate requests, retries and no waterfalls (requests that wait for each other for no reason).
6. Tell the parent in the handler, not in an effect
// ❌ useEffect(() => { onChange(value); }, [value]);// ✅ call onChange inside the same event handler that sets valueWhat effects ARE for
Use effects for: subscriptions (WebSocket, resize, matchMedia), timers, third-party widgets that you control by code (charts, maps), analytics page views, focusing an element after it shows, syncing document.title, and fetching when you have no better tool.
Custom hooks
A custom hook is a function whose name starts with use and that calls other hooks. It lets you reuse stateful logic. It does not share state: each call gets its own state.
function useOnlineStatus() { return useSyncExternalStore( (cb) => { window.addEventListener("online", cb); window.addEventListener("offline", cb); return () => { window.removeEventListener("online", cb); window.removeEventListener("offline", cb); }; }, () => navigator.onLine, () => true, // server snapshot );}A good custom hook has one clear purpose, returns a small API, and hides the effect and cleanup details.
Scenario walkthroughs
"An interval keeps logging an old count. Fix it."
Explain the stale closure. The interval callback remembered count from the first render, because the effect had empty dependencies. There are three fixes. Use a functional update to set state. Or use honest dependencies if the interval can be created again. Or use a ref or useEffectEvent to read the latest value while the interval stays the same. Also say that the lint rule would have warned about the missing dependency.
"Derived state: an effect or a plain computation?"
Use a plain computation during render (with useMemo if it is slow). An effect that copies props or state into other state adds a render. It can show a wrong screen for a moment, and it creates sync bugs. Use a key to reset state when the identity changes.
Common mistakes
- Conditional hooks, or hooks inside loops.
- Turning off the exhaustive-deps lint rule instead of fixing the closure.
- Objects and functions in dependency arrays that change on every render.
- Using effects to work out state or to react to events.
- Changing state or refs during render.
- Missing cleanup (listeners, timers, subscriptions). Strict Mode shows this problem.
- Using a ref when the screen should update (a ref does not re-render).
- Fetching in an effect without handling races, or not using a data library.
Interview questions
Why must hooks be called in the same order on every render?
React keeps a component's hook state in an ordered list. It matches each hook call to a place in the list by call order. If a hook is inside a condition or a loop, the order can change, and React would give state to the wrong hook. So hooks must always run, at the top level.
Scenario: an interval keeps logging an old count. Fix it.
The effect's callback remembered count from the render where the effect ran. The empty dependency array never refreshed it. There are three fixes. Use a functional state update (setCount(c => c + 1)). Or list count as a dependency so the effect runs again. Or read the latest value through a ref or useEffectEvent. Keep the exhaustive-deps lint rule turned on.
When do you use useLayoutEffect?
Use it when you must read the layout or change the DOM before the browser paints. An example is placing a tooltip using its measured size, so there is no visible flicker. In other cases, use useEffect. Layout effects block painting, and they do not run during server rendering.
Derived state: an effect or a plain computation?
Use a plain computation during render, with useMemo if it is slow. Copying data into state with an effect causes an extra render, a wrong screen for a moment, and sync bugs. If the source identity changes, reset the state with a key.
What is the difference between useRef and useState?
Updating state causes a re-render, and the value is part of what you see. Updating ref.current does not cause a render. Use a ref for values that do not change the screen: DOM nodes, timer IDs, "latest value" holders. Do not read or write refs during render.
What are effects for, and when should you avoid them?
Effects sync a component with outside systems: subscriptions, timers, third-party widgets, network requests. Avoid them for working out state, reacting to user events, resetting state, telling parents about changes, or chaining state updates. Use render-time calculation, event handlers, keys and reducers instead.
Why does an object in the dependency array re-run the effect every render?
React compares dependencies by reference with Object.is, which asks "is this the very same object?". An object, array or function created during render is a new object each time, so React sees a change. Move it outside the component, or create it inside the effect. If it must be passed in, keep it stable with useMemo or useCallback.
What is a custom hook and does it share state?
It is a function whose name starts with use and that calls other hooks to reuse stateful logic. It shares logic, not state. Every component that calls it gets its own state and effects.
Scenario: a component renders in an endless loop after adding an effect. What do you look for?
First, look for an effect that sets state which is also in its dependency list. Second, look for an object, array or function created during render and used as a dependency, because it changes every render. Check the exhaustive-deps lint warning. Work out values during render instead of syncing them with an effect. Make dependencies stable (move them out, or use useMemo or useCallback). Use functional updates so the effect does not need the state as a dependency.
Key takeaways
- Hooks are matched by call order. That is why the rules exist.
- Closures keep each render's values. Stale values come from missing or untrue dependencies. Fix them with functional updates, honest dependencies, refs or
useEffectEvent. - Effects sync with outside systems and need cleanup. Strict Mode checks this.
- You might not need an effect. Work out values during render, use event handlers, reset with
key, and fetch with a library or on the server. useRefis a box that does not cause renders.useLayoutEffectruns before paint.useMemoanduseCallbackare speed tools, not defaults.useReducer,useId,useSyncExternalStoreanduseeach solve one clear problem.
What's next
Lesson 02: Concurrent React, Suspense and Server Components. This is the newer half of React. It shows how React stays fast during heavy updates, streams content, and moves work to the server.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- Effects and the Lifecycle: useEffect Without the Confusion · React in Depth
- Hooks in Depth: useRef, useMemo, useCallback, useReducer and Custom Hooks · React in Depth
- “The API Is Fast but the UI Is Slow”: How to Solve It · Scenario Debugging
Part of the study guide
Related series