DrillLab
第 10 / 25 节LESSON 10 / 25约 20 分钟~20 min

Hooks 四问4 questions on Hooks

hooks 是什么与为什么、useMemo vs useCallback、React.memo vs useMemo、自定义 hook。What hooks are and why they exist, useMemo vs useCallback, React.memo vs useMemo, custom hooks.

面试 · 第 5 部分Interview · Part 5
这一页有什么On this page5
学完这节你会After this lesson you can
  • 说出 hooks 解决的三个类组件痛点Name three problems with class components that hooks solve
  • 分清 useMemo、useCallback、React.memo 各自缓存什么Say what each of useMemo, useCallback and React.memo actually caches
  • 说出 hooks 的两条规则以及「为什么」不能写在条件里State the two rules of hooks, and explain why you cannot put one inside a condition
  • 写出一个自定义 hook 并说明命名约定Write a custom hook and explain the naming rule
这在考试里考什么What the exam does with this

「useMemo 和 useCallback 有什么区别」是出现频率最高的 React 题之一,而且大部分人答不全 —— 能补上「什么时候不该用」和「三个必须配套」才是好答案。hooks 规则那道会追问底层原因(链表 + 调用顺序),答得出来就上一个档。"What is the difference between useMemo and useCallback" is one of the most common React questions, and most people give only half an answer. A good answer also covers when not to use them, and the fact that the three of them only help when used together. For the rules of hooks the interviewer will ask why, and the reason is that React stores them in a list by call order. Getting that right moves you up a level.

§01

什么是 hooks,为什么要用What are hooks, and why use them?

#324 What are hooks in React and Why do we use them

一句话:hooks 是一组use 开头的函数, 让函数组件也能有状态和副作用

解决三个真实痛点(这三条是标准答案):

  • 逻辑复用难。以前只有 HOC 和 render props, 两者都会造成 wrapper 嵌套(见 #335)。 自定义 hook 是平的。
  • 逻辑被生命周期切碎。一个「订阅 + 取消订阅」的完整逻辑 被迫拆到两个生命周期方法里;useEffect 让它们写在一起。
  • this 太容易出错。函数组件没有 this

常用的:useStateuseEffectuseContextuseRefuseMemouseCallbackuseReducer; React 18 加了useIduseTransitionuseDeferredValueuseSyncExternalStore

两条规则(必答):

  1. 只在最顶层调用—— 不能放在 if、循环、 嵌套函数里。
  2. 只在函数组件或自定义 hook 里调用。

为什么有第一条 —— 这是追问点。React 不知道你的 hook 叫什么名字, 它是按调用顺序把每个 hook 的状态 存在一条链表上的。 如果 hook 写在 if 里, 某次渲染少调了一个, 后面所有 hook 的下标就全错位了——useState 会拿到别人的值。
能答出「靠调用顺序而不是名字」就说明真理解了。

In one line: hooks are functions that start with use and give function components state and side effects.

They fix three real pain points — these three are the standard answer:

  • Reusing logic was hard. All you had were HOCs and render props, and both leave you with wrapper nesting (see #335). A custom hook is flat.
  • Logic got chopped up by the lifecycle. One coherent “subscribe and unsubscribe” had to be split across two lifecycle methods; useEffect keeps them side by side.
  • this was too easy to get wrong. Function components have no this.

The common ones: useState, useEffect, useContext, useRef, useMemo, useCallback, useReducer; React 18 added useId, useTransition, useDeferredValue and useSyncExternalStore.

Two rules — always say them:

  1. Call them at the top level only — never inside an if, a loop, or a nested function.
  2. Call them only from a function component or a custom hook.

Why the first rule exists — that is the follow-up. React does not know what your hook is called. It keeps each hook’s state in a linked list, indexed by call order. Put a hook inside an if, skip it on one render, and every index after it shifts useState hands you somebody else’s value.
Say “by call order, not by name” and they know you really understand it.

JSX为什么不能写在条件里示意Illustrative
1// ✗ 顺序会变,后面所有 hook 错位
2function Bad({ show }) {
3 if (show) {
4 const [a] = useState(1); // 有时调有时不调
5 }
6 const [b] = useState(2); // b 可能拿到 a 的槽位
7}
8
9// ✓ hook 在顶层,条件放里面
10function Good({ show }) {
11 const [a] = useState(1);
12 const [b] = useState(2);
13 useEffect(() => {
14 if (!show) return; // 条件判断放 effect 内部
15 // ...
16 }, [show]);
17}
1// ✗ the order changes, and every hook after it shifts
2function Bad({ show }) {
3 if (show) {
4 const [a] = useState(1); // called sometimes, skipped other times
5 }
6 const [b] = useState(2); // b may end up in a's slot
7}
8
9// ✓ hooks at the top level, the condition inside
10function Good({ show }) {
11 const [a] = useState(1);
12 const [b] = useState(2);
13 useEffect(() => {
14 if (!show) return; // put the condition inside the effect
15 // ...
16 }, [show]);
17}
§02

useMemo vs useCallback

#339 useMemo vs useCallback

一句话:useMemo 缓存 「函数的返回值」,useCallback 缓存 「函数本身」。两者都靠依赖数组决定要不要重算。

实际上 useCallback(fn, deps)完全等价于useMemo(() => fn, deps)——后者是前者的语法糖。 这句能答出来会加分。

缓存什么什么时候用
useMemo计算结果(值 / 对象 / 数组)① 计算真的贵(大列表排序过滤) ② 结果要当 props 传给 memo 组件 ③ 结果要当别的 hook 的依赖
useCallback函数引用① 函数要传给 memo 组件 ② 函数是 useEffect 的依赖 ③ 自定义 hook 对外暴露的函数

什么时候不该用(这半边很多人答不出):两者本身都有成本—— 要存旧值、要比较依赖。 给一个 a + buseMemo 是纯亏。「先测量,再优化」; 默认不加,profiler 显示有问题再加。

最常见的误用:包了 useCallback但依赖数组里放了每次都变的东西 —— 等于没包,还多付了比较成本。

会追问:「React 19 的编译器会怎样?」—— React Compiler 能自动插入记忆化,大部分手写的useMemo /useCallback 将不再必要。 知道这个趋势会显得你在跟进。

In one line: useMemo caches “what a function returns”, useCallback caches “the function itself”. Both use the dependency array to decide whether to recompute.

In fact useCallback(fn, deps) is exactly the same thing as useMemo(() => fn, deps) the former is sugar for the latter. Saying this scores points.

What it cachesWhen to use it
useMemoA computed result (value / object / array)① the work is genuinely expensive (sorting or filtering a big list) ② the result goes to a memo component as a prop ③ the result is a dependency of another hook
useCallbackA function reference① the function goes to a memo component ② the function is a dependency of useEffect③ the function is part of a custom hook’s public API

When not to use them — most people miss this half: both cost something — the old value is kept and the dependencies get compared. Wrapping a + b in useMemo is a pure loss. “Measure first, then optimise”: leave them out by default and add them when the profiler says so.

The most common misuse: wrapping something in useCallback but putting a value that changes every render in the dependency array — the cache never hits, and you paid for the comparison on top.

Follow-up: “What changes with the React 19 compiler?” — React Compiler can insert memoisation for you, so most hand-written useMemo and useCallback will stop being necessary. Knowing the direction shows you are keeping up.

JSX两者的区别与等价关系示意Illustrative
1// useMemo:缓存值
2const sorted = useMemo(() => items.sort(cmp), [items]);
3
4// useCallback:缓存函数
5const onPick = useCallback((id) => setPicked(id), []);
6
7// 两者的关系
8useCallback(fn, deps) === useMemo(() => fn, deps)
9
10// ✗ 常见误用:依赖每次都变,等于没缓存
11const onSave = useCallback(() => save(config), [{ ...config }]);
12// ↑ 每次都是新对象
1// useMemo caches a value
2const sorted = useMemo(() => items.sort(cmp), [items]);
3
4// useCallback caches a function
5const onPick = useCallback((id) => setPicked(id), []);
6
7// How the two relate
8useCallback(fn, deps) === useMemo(() => fn, deps)
9
10// ✗ a common mistake: the dependency changes every time, so nothing is cached
11const onSave = useCallback(() => save(config), [{ ...config }]);
12// ↑ a new object every render
§03

React.memo vs useMemo

#346 React.memo vs useMemo

一句话:React.memo 是 「组件」级的 —— 决定要不要重新渲染整个组件useMemo 是 「值」级的 —— 决定要不要重新计算一个值

React.memouseMemo
是什么高阶组件hook
用在哪包在组件外面写在组件里面
比较什么props(浅比较)依赖数组
省掉什么一次组件渲染一次计算

关键:三个必须配套用。只在子组件上加 React.memo通常一点效果都没有—— 因为父组件每次渲染都会给出新的对象和函数字面量, 浅比较必然判定「变了」。必须同时用useMemo 稳住对象、useCallback 稳住函数。

会追问:memo 能拦住 context 变化吗?」——拦不住memo 只比 props, context 走另一条通道。所以 context value 必须useMemo—— 这正是我们那道主题切换变式题的核心考点, 删掉 useMemo 后实测「功能测试全绿、 只有引用稳定性那条红」。
children 会破坏 memo 吗?」—— 会,children 也是 prop, 而 JSX 每次都产生新元素对象。

In one line: React.memo works at the “component” level — it decides whether to re-render a whole component; useMemo works at the “value” level — it decides whether to recompute one value.

React.memouseMemo
What it isA higher-order componentA hook
Where it goesWrapped around the componentInside the component
What it comparesprops (shallow)the dependency array
What it savesone component renderone computation

The key point: all three go together. Dropping React.memo on a child usually does nothing at all — the parent hands down fresh object and function literals on every render, so the shallow compare is bound to say “changed”. You must also stabilise objects with useMemo and functions with useCallback.

Follow-up: “Can memo stop a context change?” — no it cannot. memo only compares props; context travels a separate channel. That is why a context value has to be wrapped in useMemo — exactly the point of our theme switching variant, where deleting the useMemo left every behaviour test green and only the reference-stability test red.
“Does children break memo?” — yes. children is a prop too, and JSX produces a new element object every time.

§04

自定义 hook 是干什么的,命名有什么约定What is a custom hook for, and what is the naming rule?

#340 What are custom hooks for and what is the naming convention for them

一句话:「带状态的逻辑」抽出来复用。 命名必须以 use 开头

为什么必须 use 开头—— 这是考点,不是风格问题:

  • ESLint 靠这个前缀识别它是 hook, 才能检查 hooks 规则 (react-hooks/rules-of-hooks)。 不加前缀,你在里面违规调用 hook 也不会有人警告你。
  • 它同时也是给读代码的人的信号:这个函数里可能有状态, 所以它有调用位置的限制

关键概念:复用的是逻辑,不是状态。两个组件各自调 useCounter(), 得到的是两份完全独立的状态。 想共享状态得用 Context 或状态库。这一条是高频追问,很多人答错。

什么时候该抽:同一组useState +useEffect 的组合在两处以上出现; 或者一个组件里的 effect 逻辑长到 让主体读不懂了。

会追问:「自定义 hook 能返回什么?」—— 随意。约定是「像 useState 一样返数组」 (调用方好重命名)、 「三个以上返对象」(不用记顺序)。
「里面能调别的 hook 吗?」—— 能,这正是它的意义;但同样要遵守两条规则。

In one line: pull “logic that carries state” out so it can be reused. The name must start with use.

Why the use prefix is mandatory — this is the point being tested, and it is not about style:

  • ESLint uses the prefix to recognise it as a hook so it can enforce the rules of hooks (react-hooks/rules-of-hooks). Without the prefix, you can break those rules inside it and nobody warns you.
  • It is also a signal to whoever reads the code: this function may hold state, so there are limits on where you may call it.

The key idea: you reuse the logic, not the state. Two components that each call useCounter() get two completely independent pieces of state. To share state you need Context or a state library. This is a frequent follow-up and a lot of people get it wrong.

When to extract one: the same combination of useState and useEffect shows up in two or more places; or the effect logic in one component has grown long enough that you can no longer read the component itself.

Follow-up: “What can a custom hook return?” — anything. The convention is “return an array like useState does” (so the caller can rename freely) and “return an object once there are three or more values” (so nobody has to remember the order).
“Can it call other hooks?” — yes, that is the whole point; the same two rules still apply.

JSX自定义 hook 的形状示意Illustrative
1// 一个真实好用的:把「值 + 存 localStorage」打包
2function useLocalStorage(key, initial) {
3 const [value, setValue] = useState(() => {
4 try {
5 const raw = localStorage.getItem(key);
6 return raw ? JSON.parse(raw) : initial;
7 } catch {
8 return initial; // 隐私模式读不了就用默认值
9 }
10 });
11
12 useEffect(() => {
13 try { localStorage.setItem(key, JSON.stringify(value)); } catch {}
14 }, [key, value]);
15
16 return [value, setValue]; // 像 useState 一样返数组
17}
18
19// 用起来
20const [theme, setTheme] = useLocalStorage("theme", "light");
21
22// 注意:两个组件各调一次,得到的是两份独立状态,不是共享的
1// One that is genuinely useful: a value together with storing it in localStorage
2function useLocalStorage(key, initial) {
3 const [value, setValue] = useState(() => {
4 try {
5 const raw = localStorage.getItem(key);
6 return raw ? JSON.parse(raw) : initial;
7 } catch {
8 return initial; // private mode cannot read, so use the default
9 }
10 });
11
12 useEffect(() => {
13 try { localStorage.setItem(key, JSON.stringify(value)); } catch {}
14 }, [key, value]);
15
16 return [value, setValue]; // returns an array, the same shape as useState
17}
18
19// Using it
20const [theme, setTheme] = useLocalStorage("theme", "light");
21
22// Note: two components each calling it get two separate states, not a shared one
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

hook 写在 if 里A hook written inside an if
React 按调用顺序存链表,会错位;条件放 effect 内部React stores hooks in a list by call order, so they shift out of line; put the condition inside the effect instead
问 useMemo vs useCallbackAsked about useMemo vs useCallback
一个缓存值一个缓存函数;后者是前者的语法糖One caches a value, the other caches a function; useCallback is a shorter way to write useMemo
加了 memo 没效果You added memo and nothing got faster
三个必须配套;context 变化 memo 拦不住All three have to be used together, and memo cannot stop a re-render caused by a Context change
同一组 state+effect 出现两次The same pair of state and effect appears in two places
抽自定义 hook,use 开头Pull it out into a custom hook whose name starts with use
以为自定义 hook 能共享状态Expecting a custom hook to share state between components
复用的是逻辑,状态各自独立The logic is reused; each caller gets its own separate state
这节的要点What to take away
  1. hooks 解决三件事:逻辑复用难、逻辑被生命周期切碎、this 易错。hooks solve three things: reusing logic was hard, logic was cut apart across lifecycle methods, and this was easy to get wrong.
  2. hooks 规则的底层原因是「按调用顺序存链表」,不是按名字。The reason for the rules of hooks is that React stores them in a list by call order, not by name.
  3. useMemo 缓存值、useCallback 缓存函数;useCallback 就是 useMemo(() => fn, deps)。useMemo caches a value, useCallback caches a function; useCallback is exactly useMemo(() => fn, deps).
  4. React.memo 省一次渲染、useMemo 省一次计算;三个必须配套用才有意义。React.memo saves a render, useMemo saves a computation; the three of them only help when used together.
  5. memo 拦不住 context 变化 —— 所以 context value 必须 useMemo。memo cannot stop a Context change, which is why a Context value must go through useMemo.
  6. 自定义 hook 必须 use 开头(ESLint 靠它识别);复用逻辑不复用状态。A custom hook has to start with use, because that is how ESLint recognises it; you reuse the logic, not the state.

接下来What next

  1. 接着看下一节Continue to the next lesson性能与新特性 · 八问8 questions on performance and new features
    下一节Next lesson
  2. 可选:再巩固一下Optional: reinforce it这一节的 4 道八股4 questions from this lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 组件与通信 · 十一问11 questions on components and how they communicate