React in Depth

Hooks in Depth: useRef, useMemo, useCallback, useReducer and Custom Hooks

Each hook with one small reason to use it, the rules of hooks and why they exist, and how to write a custom hook.

Lesson 0316 min read#react#hooks#useReducer#custom-hooks

On this page

  1. Words used in this lesson
  2. How hooks work, with a 15-line engine
  3. useRef: a box that React does not watch
  4. useMemo: remember a calculation
  5. useCallback: remember a function
  6. useReducer: one place for all the ways state can change
  7. Custom hooks: share logic, not state
  8. useSyncExternalStore: reading things that live outside React
  9. The use API (React 19)
  10. useId, in one paragraph
  11. Which hook for which job
  12. Try it
  13. Interview questions
  14. Key takeaways
  15. What's next

In one line: a hook is a function, whose name starts with use, that lets a component plug into a React feature: memory, effects, refs, context, caching. Hooks work because React remembers them by the order they are called, so you must call them in the same order on every render. A custom hook is just your own function that calls other hooks, to share logic and not state.

You already know useState and useEffect. This lesson adds the rest of the everyday set: useRef, useMemo, useCallback, useReducer, and the new use from React 19. For each I give one honest reason to use it, and one test I ran. Then comes the part many people skip: why the rules of hooks exist. I wrote a tiny hook engine in 15 lines to show it, and I broke the rules in real React to show you the real error message.

The previous lesson was Effects and the Lifecycle. The next one, Forms, Events and Controlled Inputs, puts hooks to work in forms.

What I ran / checked. Node 22.22.0, React 19.3.0, react-dom 19.3.0, jsdom 29.1.1, esbuild 0.28.2, development build. Every output below came from components mounted with createRoot in jsdom and wrapped in act(), except the mini hooks engine, which is a plain script. I read these pages from the reactjs/react.dev repository (raw markdown): Rules of Hooks, Referencing Values with Refs, Extracting State Logic into a Reducer, Reusing Logic with Custom Hooks, and the reference pages for useRef, useMemo, useCallback, useReducer, useId and use. I did not run the React Compiler in this lesson (lesson 7 talks about it, and says what I did and did not run). Code and explanations are in my own words. Warning wording can change between React versions.

Words used in this lesson

  • Hook is a function starting with use that React lets you call only while rendering a component or another hook.
  • Rules of Hooks are the two rules: call hooks only at the top level (not in loops, conditions or nested functions), and only from React functions.
  • Ref is a plain box { current: value } that React keeps between renders. Changing it does not cause a re-render.
  • Memoization means remembering the result of a calculation and reusing it when the inputs are the same.
  • Reducer is a pure function (state, action) => newState. It holds all the ways a piece of state can change in one place.
  • Dispatch is the function you call to send an action to a reducer.
  • Custom hook is your own function whose name starts with use and that calls other hooks.
  • Suspense is a React feature that shows a fallback (like a spinner) while some part of the tree is not ready.
  • Thenable is an object with a .then method, like a promise.
  • External store is state that lives outside React (a browser API or a library store) that components can subscribe to.

How hooks work, with a 15-line engine

Look at this component:

JavaScript
function Profile({ admin }) {  const [name, setName] = useState("Ada");  const [age, setAge] = useState(30);  // ...}

useState takes no name. How does React know that the second call belongs to age? By order. For each component instance, React keeps an ordered list of "slots". The first hook call uses slot 0, the second uses slot 1, and so on. Each render, the counter starts at 0 again and walks through the list.

To make this concrete, I wrote a minimal version of the idea. It is not React. It is only the shape of the trick:

JavaScript
function miniReact() {  let hooks = [], idx = 0;  function useState(init) {    const i = idx++;    if (!(i in hooks)) hooks[i] = init;    return [hooks[i], v => { hooks[i] = v; }];  }  function render(C, props) { idx = 0; return C(props); }  return { useState, render };}

hooks is the list of slots, and idx is "which hook am I on now". Now a component that breaks the rule by calling a hook inside an if:

JavaScript
function Profile({ admin }) {  if (admin) { const [role] = R.useState("admin-role"); }  const [name, setName] = R.useState("Ada");  return name;}console.log(R.render(Profile, { admin: true }));    // first renderconsole.log(R.render(Profile, { admin: false }));   // second render, hook skipped
TEXT
B. mini hooks, first render admin=true : Ada   second render admin=false (hook skipped): admin-role  <- got the role slot

On the first render there were two hooks: role in slot 0, name in slot 1. On the second render the if was false, so useState("Ada") became the first call and read slot 0. It got "admin-role" and not "Ada". Everything after that is shifted by one. This is the whole reason for the rule: the list is matched by position, so the positions must not change.

Real React notices this and tells you. I did the same with real React, a component that calls useState only when a prop is true, rendered first with show={false} and then with show={true}:

TEXT
A. conditional hook -> thrown: Rendered more hooks than during the previous render.   console.error says: React has detected a change in the order of Hooks called by %s.   This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: ...

Real React threw an error and also printed a table of the old and new hook order in the console. (The other direction, calling fewer hooks, is also an error.)

The rules, in plain words

From the official Rules of Hooks page:

  1. Call hooks at the top level only. Not inside if, loops, try/catch, event handlers, or functions passed to useMemo, useEffect or useReducer. Not after an early return.
  2. Call hooks only from components or custom hooks. Not from plain functions and not from class components.

If you need a hook "sometimes", call it always and use the result sometimes. Or move the conditional part into a separate component, which is only rendered when needed. The linter plugin eslint-plugin-react-hooks checks these rules, and your project template probably has it on already.

useRef: a box that React does not watch

useRef(initial) returns an object { current: initial } that stays the same object for the life of the component. You can read and write current whenever you want. Writing to it does not cause a re-render. I compared it with state:

JavaScript
function RefVsState() {  const clicks = useRef(0);  const [shown, setShown] = useState(0);  L(`render: ref=${clicks.current} state=${shown}`);  return <div>    <button id="r" onClick={() => { clicks.current += 1; L("ref now " + clicks.current); }}>ref</button>    <button id="s" onClick={() => setShown(shown + 1)}>state</button>  </div>;}
TEXT
1. mount  render: ref=0 state=02. click ref twice (no render)  ref now 1  ref now 23. click state (render shows the ref value)  render: ref=2 state=1

Two clicks on the "ref" button: the ref changed, no render happened. The "state" button caused a render, and that render read the ref's current value, 2. Refs are for values that React does not need to display, but the component needs to remember:

  • an interval or timeout ID, so you can clear it later,
  • the previous value of something,
  • a flag like "has the first fetch run",
  • a DOM element (the most common use).

The simple test from the docs: if the value is shown on the screen, it should be state. If it is only bookkeeping, a ref is right. Do not read or write ref.current during rendering, because it makes rendering impure (the docs explain this). Read and write it in event handlers and effects.

Refs to DOM elements

Put a ref on a tag, and React sets current to the real DOM node after the commit:

JavaScript
function Form() {  const r = useRef(null);  useEffect(() => { r.current?.focus(); }, []);  return <input ref={r} />;}

This is the way to focus an input, scroll to something, or measure it. It is an escape hatch for things React can not do declaratively. Avoid changing the DOM's structure through a ref, because React will not know and may undo it.

React 19: ref is a plain prop

In older React, a function component could not receive ref as a prop. You had to wrap it in forwardRef. In React 19 a function component receives ref like any other prop. I tested a custom input component:

JavaScript
function FancyInput({ ref, label }) {      // no forwardRef  return <label>{label}<input ref={ref} /></label>;}function Form() {  const r = useRef(null);  useEffect(() => {    L("ref in effect: " + r.current?.tagName);    r.current?.focus();    L("focused is input: " + (document.activeElement === r.current));  }, []);  return <FancyInput ref={r} label="Name" />;}
TEXT
C. ref in effect: INPUT | focused is input: true

It worked with no forwardRef. (forwardRef is still exported from React 19.3.0 for old code, but new code does not need it. The docs for 19 tell you ref is a prop for function components.)

React 19 also lets a ref callback return a cleanup function, which runs when the node is removed. Like an effect, in a few lines:

JavaScript
<div ref={(el) => { L("ref attach " + el.tagName); return () => L("ref cleanup"); }} />
TEXT
callback ref with cleanup: ref attach DIV | ref cleanup

Mounted, then unmounted: attach, then cleanup. This is neat for things like observers (ResizeObserver) that you start when a node appears and stop when it goes.

useMemo: remember a calculation

useMemo(fn, deps) calls fn during render and stores the result. On the next render, if none of the deps changed (compared with Object.is), it returns the stored result and does not call fn.

JavaScript
function Search() {  const [q, setQ] = useState("a");  const [dark, setDark] = useState(false);  const items = ["apple", "banana", "cherry"];  const hits = useMemo(() => slowFilter(items, q), [q]);  // ...}

I counted how often the filter function ran:

TEXT
4. useMemo: computes after mount = 1   after theme toggle = 1   after q changes = 2

Toggling the theme re-rendered the component, but the filter did not run again, because q was the same. Changing q ran it. That is all useMemo does.

When is it worth using? The docs list three honest reasons:

  1. The calculation is really slow (you measured it, and it is noticeable).
  2. The result is passed to a component wrapped in memo, and you need the same object identity so it can skip re-rendering.
  3. The result is a dependency of another hook (like useEffect), and a new identity each time would re-run it (lesson 2: object dependencies).

For a cheap calculation, useMemo is not free: it costs memory and comparison time. Beginners tend to wrap everything. Do not. Measure first (lesson 7 shows how). And it is a performance hint, not a promise: React may throw away cached values (for example, in the future when offscreen content is dropped), so your code must still be correct if the function runs again. In Strict Mode, the function you pass runs twice on mount in development. I checked:

TEXT
D. useMemo callback calls in StrictMode on mount: 2

useCallback: remember a function

useCallback(fn, deps) is useMemo(() => fn, deps) with a nicer name: it returns the same function object while the dependencies are the same. Why would you want the same function? Because of identity. Recall lesson 1: memo skips a child if its props are equal, and a function created in the body is a new one on each render. So an inline function breaks memo:

JavaScript
const Item = memo(function Item({ onPick }) { L("Item render"); return <button onClick={onPick}>pick</button>; });function NoCb() {  const [n, setN] = useState(0);  const onPick = () => {};                          // new function each render  return <div><button onClick={() => setN(n + 1)}>{n}</button><Item onPick={onPick} /></div>;}function WithCb() {  const [n, setN] = useState(0);  const onPick = useCallback(() => {}, []);         // same function each render  return <div><button onClick={() => setN(n + 1)}>{n}</button><Item onPick={onPick} /></div>;}

I clicked the counter in each, after clearing the log:

TEXT
5. memo child, inline function prop, parent re-renders  Item render6. memo child, useCallback prop, parent re-renders  (nothing)

With a plain function, the memo child rendered again (the prop changed identity). With useCallback, it did not. The pair works together only when both pieces are present: memo on the child and a stable prop.

useCallback alone does nothing useful. If the child is not wrapped in memo, or does not use the function in a dependency array, the stable identity is wasted. People add useCallback to every function "just in case". The docs say it only matters for the cases above. A newer option is the React Compiler (lesson 7), which adds this kind of memoization automatically at build time, so you write plain code.

useReducer: one place for all the ways state can change

When state has several fields and several different ways to change it, a pile of useState and handlers gets messy. useReducer moves the logic into one reducer function:

JavaScript
function reducer(state, action) {  switch (action.type) {    case "add":   return { ...state, items: [...state.items, action.text] };    case "clear": return { ...state, items: [] };    default: throw new Error("Unknown action: " + action.type);  }}function Todo() {  const [state, dispatch] = useReducer(reducer, { items: [] });  return (    <div>      <button onClick={() => dispatch({ type: "add", text: "task" + (state.items.length + 1) })}>add</button>      <button onClick={() => dispatch({ type: "clear" })}>clear</button>      <p>{state.items.join(",")}</p>    </div>  );}

I clicked "add" twice, then "clear":

TEXT
7. reducer: task1,task2 | distinct dispatch functions seen: 1   after clear: ""

Two things to notice. First, the event handlers now only describe what happened ({ type: "add" }). The how is in the reducer. Second, I stored every dispatch function I saw across renders, and there was just one. dispatch has a stable identity, so you can pass it to children without useCallback and list it in dependency arrays at no cost.

Reducer rules: it must be pure (no fetching, no random numbers, no mutation: return a new object), and it runs twice in Strict Mode like other render-time code. useReducer is the same engine as useState, with a different shape. I pick it when: the next state depends on the previous in complicated ways, many handlers change the same state, or I want to test the logic without React (a reducer is just a function, so a unit test is three lines).

Custom hooks: share logic, not state

When two components need the same combination of hooks, you write a function. If its name starts with use and it calls other hooks, it is a custom hook:

JavaScript
function useToggle(initial = false) {  const [on, setOn] = useState(initial);  return [on, () => setOn(o => !o)];}function Toggle({ name }) {  const [on, toggle] = useToggle();  return <button onClick={toggle}>{name}:{String(on)}</button>;}// <Toggle name="A" /> and <Toggle name="B" /> side by side

I clicked only A:

TEXT
E. two components, same hook: A:true  B:false

Each call to a hook creates its own state. The hook shares the code, not the data. This is the most common misunderstanding. If you want to share data between components, put the state in a common parent (lesson 6) or in a store.

Why the use prefix? So the linter (and humans) can tell the function calls hooks and therefore must follow the rules of hooks.

Two more custom hooks that I ran, both from real projects:

useLocalStorage reads the saved value once (with the lazy initial state trick from lesson 1) and saves on change:

JavaScript
function useLocalStorage(key, initial) {  const [v, setV] = useState(() => {    const raw = window.localStorage.getItem(key);    return raw === null ? initial : JSON.parse(raw);  });  useEffect(() => { window.localStorage.setItem(key, JSON.stringify(v)); }, [key, v]);  return [v, setV];}

I clicked a theme button, then mounted a new copy of the component:

TEXT
button: dark | saved: "dark"new mount reads saved value: dark

useDebounce delays a value, so you do not search on every keystroke:

JavaScript
function useDebounce(value, ms) {  const [d, setD] = useState(value);  useEffect(() => {    const id = setTimeout(() => setD(value), ms);    return () => clearTimeout(id);  }, [value, ms]);  return d;}

I typed five characters, 5 ms apart, with a 30 ms debounce:

TEXT
5 fast keystrokes -> searches fired for: [ 'r', 'react' ]

The search fired for the first value (on mount) and then only for the final "react". The cleanup cancelled every timer that was replaced. Notice the effect cleanup from lesson 2 doing real work.

Good custom hook habits:

  • Name what it does: useOnlineStatus, not useThing.
  • Return what the caller needs: a value, a pair [value, setValue], or an object. Keep it small.
  • A custom hook should not secretly depend on a specific component. Pass what it needs as arguments.
  • Do not create "lifecycle" hooks like useMount(fn). They hide the dependency list and fight the linter.

useSyncExternalStore: reading things that live outside React

Some data lives outside React: window.innerWidth, navigator.onLine, or a store library. If you copy it into state with an effect, you may get a flash of wrong data. React gives a dedicated hook, useSyncExternalStore(subscribe, getSnapshot). You give it a function to subscribe (it must return an unsubscribe function) and a function to read the current value.

JavaScript
let value = 1; const listeners = new Set();const store = {  get: () => value,  subscribe: (cb) => { listeners.add(cb); return () => listeners.delete(cb); },  set: (v) => { value = v; listeners.forEach(l => l()); },};function Show() { const v = useSyncExternalStore(store.subscribe, store.get); return <p>store = {v}</p>; }
TEXT
<p>store = 1</p> | listeners: 1<p>store = 2</p>listeners after unmount: 0

React subscribed on mount, re-rendered when the store changed, and unsubscribed on unmount. It also guarantees the whole tree sees the same value in one render (no "tearing"). Libraries like Zustand and Redux use this hook inside. Most app code will use it through them.

The use API (React 19)

use is not quite a hook, and not quite a normal function. It reads a value from a promise or from a context, and it may be called inside if and loops, which hooks may not. Two uses:

1. Read a promise, with Suspense. If the promise is not finished, the component suspends: React shows the nearest <Suspense fallback> until it resolves, then renders again with the value. If the promise rejects, the nearest error boundary handles it (lesson 8).

JavaScript
const userPromise = new Promise(r => setTimeout(() => r({ name: "Ada" }), 40));function User({ p }) { const u = use(p); return <h1>{u.name}</h1>; }<Suspense fallback={<p>loading...</p>}>  <User p={userPromise} /></Suspense>
TEXT
t=0   : <p>loading...</p> | User render attempts: 2t=80ms: <h1>Ada</h1> | User render attempts: 3

First the fallback, then the real content once the promise resolved. Note that User was attempted more than once. A component that suspends is called again later, so keep the render pure. The important rule: the promise must be created outside the render, or cached, and passed in. If you create a new promise in the render body, each attempt makes a new pending promise and it can never finish. I tried, with a new promise per render:

TEXT
new promise each render -> loading | renders: 3Error: This library called use() to suspend in a previous render but did not call use() when it finished.This indicates an incorrect use of use(). Learn more: https://react.dev/warnings/conditional-use-of-use

It stayed on "loading" and React printed a warning. Real code gets the promise from a framework, a cache, a router loader, or a parent that created it once (for instance a Server Component passing a promise down). Lesson 5 covers data fetching patterns.

2. Read context, even conditionally. useContext must follow the rules of hooks. use(Context) may be called after an early return:

JavaScript
const Theme = createContext("light");function Panel({ open }) {  if (!open) return <p>closed</p>;  const t = use(Theme);                 // fine after an early return  return <p>theme={t}</p>;}
TEXT
<p>theme=dark</p><p>closed</p>

use is allowed in conditions, but it must still be called while rendering a component or hook, not in an event handler or in an effect. Think of it as "unwrap this value, wait if needed".

useId, in one paragraph

useId() returns a string that is unique (in React 19.3.0 server output I got _R_1_ and _R_2_, but the format is not a promise, so never parse it) in the app and the same on the server and the client. Use it for id / htmlFor / aria-describedby pairs, especially in reusable components that may appear many times on a page. Do not use it as a list key. We use it in lesson 4.

Which hook for which job

NeedHook
Remember something and show ituseState
Several related state changes, logic in one placeuseReducer
Remember something without re-rendering; hold a DOM nodeuseRef
Keep a slow calculation's resultuseMemo
Keep a function's identity for a memo child or a dependencyuseCallback
Connect to something outside ReactuseEffect
Read a non-React storeuseSyncExternalStore
Read a promise or context (with Suspense)use
Unique ID for accessibilityuseId
Share any of the above between componentsA custom hook

Try it

  1. Write a component with if (props.big) { const [x] = useState(0) } then toggle big. What error do you get? Move the hook so it is always called.
  2. Make a "stopwatch" that stores the setInterval ID in a useRef so a Stop button can clear it. Why not use state for the ID?
  3. Count how many times an expensive function runs when you toggle an unrelated piece of state. Wrap it in useMemo. Count again.
  4. Create memo(Child) with an onClick prop. Show that the child re-renders on every parent update. Fix it with useCallback. What happens if you remove memo but keep useCallback?
  5. Convert a counter with increment, decrement and reset from useState to useReducer.
  6. Write useOnlineStatus with useSyncExternalStore and navigator.onLine and the online / offline events.
  7. Write useToggle and use it twice in one component. Do the two toggles share state?
  8. Call use(promiseCreatedInTheComponent). What happens?

Answers

  1. "Rendered more hooks than during the previous render." Always call the hook and use its result inside the if.
  2. An ID is bookkeeping and is not shown on screen. Setting it as state would trigger a pointless re-render, while a ref changes silently.
  3. Without useMemo, the function runs on every render. With it, only when its inputs change.
  4. With memo and a plain function, the child re-renders on every parent update. With useCallback it does not. Without memo, the child still re-renders and useCallback gains nothing.
  5. State becomes { count }. Actions are {type: "inc"}, {type: "dec"}, {type: "reset"}. The reducer returns new states.
  6. subscribe adds the two event listeners and returns a function that removes them. getSnapshot returns navigator.onLine.
  7. No. Each call to a hook has its own state.
  8. The component stays suspended and React warns, because each render makes a new promise. Create it once outside, or cache it.

Interview questions

What are the rules of hooks, and why do they exist? (Must know)

Call hooks only at the top level of a component or custom hook (not in conditions, loops, nested functions or after an early return), and only from React functions. They exist because React stores hook state in an ordered list per component and matches each call to its slot by position. If the order changes between renders, the slots shift and I get another hook's data. I showed this with a 15-line engine: skipping one useState made the next one read the wrong slot. Real React throws "Rendered more hooks than during the previous render."

What is the difference between useRef and useState? (Must know)

State is for values that appear on screen. Changing it triggers a re-render. A ref is a stable { current } box. Changing current does not trigger a re-render and React does not watch it. I use a ref for timer IDs, previous values and DOM nodes, and I don't read or write it during render. In my test, two ref clicks produced no render, and the next state-driven render showed the updated value.

When should I use useMemo and useCallback? (Must know)

When I measured a slow calculation and want to skip it, when I need a stable object or function so a memo-wrapped child can skip rendering, or when the value is a dependency of an effect and a new identity each render would re-run it. Not for cheap code. I showed that a memo child skipped re-rendering only when it got a useCallback function: with an inline function it rendered again, and with useCallback it did not. And useCallback without memo does nothing for the child.

Is useMemo a guarantee? (Senior signal)

No, it is a performance hint. React may discard cached values, and in Strict Mode it calls my function twice on mount in development (I saw 2 calls). So my code must be correct if the function runs again, and must have no side effects. If it needs to be correct (not just fast), I should not rely on it.

When do I choose useReducer over useState? (Senior signal)

When state has many fields that change together, when many handlers change the same state in different ways, or when I want pure logic that I can unit test without a component. The handlers dispatch small actions that describe what happened, and the reducer holds the how. dispatch is stable, so passing it down is cheap. For a single number or flag, useState is simpler.

Do two components that use the same custom hook share state? (Trap)

No. Each call to the hook creates separate state. A custom hook shares code, not data. In my test, two toggles built with the same useToggle had values A:true and B:false after clicking only A. To share data, I lift state to a common parent, use context, or use an external store.

What is new about refs in React 19? (Senior signal)

Function components receive ref as a normal prop, so forwardRef is no longer needed. forwardRef still exists for old code. Also, ref callbacks can return a cleanup function, which React calls when the node is removed, similar to an effect. I tested both: a custom input got focused through a ref prop with no wrapper, and a callback ref logged "attach" and then "cleanup" on unmount.

What is the use API, and how is it different from a hook? (Senior signal)

use reads a promise or a context. With a promise, it suspends the component until the promise resolves, and the nearest Suspense shows a fallback. Unlike hooks, it can be called conditionally and in loops. It still has to be called during render. The promise must be stable across renders (created outside, cached, or passed from a parent that made it once). When I created a new promise in the render body, the component never finished and React warned about incorrect use of use().

How do I read browser state like navigator.onLine safely? (Senior signal)

With useSyncExternalStore(subscribe, getSnapshot). subscribe registers a listener and returns the unsubscribe function, and getSnapshot reads the current value. React subscribes, re-renders on change, unsubscribes on unmount, and makes sure all components see the same value in one render. Copying it into state with an effect can cause one frame of stale data and tearing. In my test the store listener count was 1 while mounted and 0 after unmount.

Why does a custom hook's name need to start with use? (Must know)

It is a convention that tools rely on. The lint rules know that a function with that name calls hooks and so must follow the rules of hooks, and they apply the checks to its callers. If I call hooks inside a function named getTheme, the linter won't enforce the order rules for it, and I could call it conditionally by accident.

How would you debounce a search input in React? (Senior signal)

I write a useDebounce(value, ms) hook with state and an effect: set a timeout that updates the debounced state, and clear it in the cleanup. The component uses the debounced value for the fetch. In my test, five fast keystrokes produced searches only for the first value and the last. An alternative in React 19 is useDeferredValue, which lets React defer the less urgent update without a fixed delay, but it doesn't cancel network requests the way a debounce does.

Can a hook be called inside an event handler or a useEffect callback? (Trap)

No. Hooks have to run during rendering in the same order every time. Inside an event handler or effect callback, the call happens later and in no fixed order. If I need a hook's value in a handler, I call the hook at the top level and use the returned value (or function) inside the handler.

Key takeaways

  • Hooks are matched by call order, so call them unconditionally at the top level. Breaking the order gives "Rendered more hooks than during the previous render."
  • useRef holds a value that survives renders without causing one, and holds DOM nodes. In React 19 ref is a normal prop for function components.
  • useMemo and useCallback keep values and functions stable. Use them for slow work, memo children and effect dependencies, after measuring.
  • useReducer gathers all state changes in a pure function; its dispatch is stable.
  • A custom hook shares logic and not state. Each call gets its own.
  • useSyncExternalStore reads outside stores safely. use unwraps promises and context, can be conditional, and needs a stable promise.

What's next

Next: Forms, Events and Controlled Inputs, lesson 4. We use these hooks to build forms: controlled and uncontrolled inputs, the React 19 form action, useActionState, useFormStatus, useOptimistic, validation and accessibility. I ran a form in a real browser and recorded the pending state, the error, and the reset behaviour. Practise now: take a component of yours that has a useEffect with a setInterval and move it into a custom hook with a clear name.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Related lessons

Part of the study guide

Related series