DrillLab
八股题库Question bank

105 道问答题,一道一卡105 questions, one card each

默认只显示问题 —— 先自己在心里答一遍,再展开对答案。答不上来就标「不会」,下次抽认卡会先抽它。Only the question shows by default. Answer it in your head first, then open the answer. Mark the ones you miss; the flashcard round puts those first.

0 / 105道自评过self-assessed
0Got it0模糊Shaky0不会No idea105还没做Not seen
正在读这台浏览器里的标记…Reading your marks from this browser…
标记不是打分,是给下一轮排队Marks are a queue, not a score

标「不会」的题会排到抽认卡最前面,标「会」的进低频池排最后。所以别客气 —— 觉得答得磕磕巴巴就标「模糊」。准备好了就去抽认卡“No idea” jumps to the front of the next round; “got it” drops to the back. So be honest — if it came out shaky, mark it shaky. Then go run a flashcard round.

题目Questions

筛出 105 道 · 第 6 / 9 页。105 of 105 questions · page 6 / 9.
React 与生态React & ecosystem#325

useEffect 和生命周期怎么对应

UseEffect vs Lifecycle Methods

看答案Show answer

一句话(这句要说准):useEffect不是生命周期的替代品, 它是「同步副作用」的另一种思路—— 你声明「这个副作用依赖哪些值」, 值变了它就重新跑。

类组件useEffect 写法
componentDidMountuseEffect(fn, [])
componentDidUpdateuseEffect(fn, [dep])
componentWillUnmounteffect 里 return () => {}
三个都要useEffect(fn)(不写依赖数组)
getSnapshotBeforeUpdateuseLayoutEffect(DOM 更新后、浏览器绘制前同步执行)

但这张表有个陷阱——useEffect(fn, [])不完全等于 componentDidMount: 前者在浏览器绘制之后异步执行,后者是同步的。所以用 useEffect测量 DOM 再改样式会闪一下, 这种情况要用useLayoutEffect

更重要的是别用生命周期的思维写 effect。正确的问法不是「我要在挂载时干什么」, 而是「这个副作用依赖哪些值」。 依赖列全,React 自然会在该跑的时候跑。

会追问:「清理函数什么时候执行?」——依赖变化前卸载时我们那道计时器变式题就是这个考点: 漏了 clearInterval, start/pause 四次会得到 10 秒而不是 4 秒(实测)。

In one line — say this precisely: useEffect is not a replacement for lifecycle methods; it is a different way of thinking about synchronising side effects — you declare which values a side effect depends on, and it re-runs when they change.

Class componentuseEffect form
componentDidMountuseEffect(fn, [])
componentDidUpdateuseEffect(fn, [dep])
componentWillUnmountreturn () => {} inside the effect
All three at onceuseEffect(fn) (no dependency array)
getSnapshotBeforeUpdateuseLayoutEffect (runs synchronously after the DOM updates, before the browser paints)

But the table has a trap useEffect(fn, []) is not quite componentDidMount: the first runs asynchronously after the browser paints, the second is synchronous. So measuring the DOM in a useEffect and then changing styles will flash; that case wants useLayoutEffect.

More important: stop writing effects with a lifecycle mindset. The right question is not “what do I do on mount” but “which values does this side effect depend on”. List the dependencies properly and React runs it when it should.

Follow-up: “When does the cleanup function run?” — before the dependencies change and on unmount. Our timer variant question tests exactly this: drop the clearInterval and four start/pause rounds give you 10 seconds instead of 4 (measured).

React 与生态React & ecosystem#327

props vs state

props vs state

看答案Show answer

一句话:props 是父组件传进来的、只读state 是组件自己的、可变

propsstate
谁拥有父组件组件自己
能否修改不能(只读)能,通过 setState
变了会重渲染吗

为什么 props 必须只读?因为组件的渲染函数应该是纯的(见 #293):同样的 props 渲染出同样的 UI。 改了 props 就等于改了「输入」, 父组件下次渲染又会把它覆盖回去 —— 数据源变成两个,谁也说不清现在该信谁。

怎么判断该用哪个(实用判据):

  • 能从 props 或别的 state 算出来都别放,直接算(派生数据)
  • 只有这个组件关心、且会变 → state
  • 多个组件都要用 → 提到共同父级(#345)
  • 整棵树都要用且不常变 → Context

会追问:「能把 props 存进 state 吗?」——能但通常是 buguseState(props.value)只在首次渲染取值, 之后 props 变了 state 不会跟着变。 只有「需要一个可编辑的初始值」时才这么做, 而且要想清楚 props 变化时要不要重置。Q1 那道题的编辑功能就是这个场景, 它用 useEffect 显式同步。

In one line: props come from the parent and are read-only; state belongs to the component and can change.

propsstate
Who owns itThe parentThe component itself
Can you change itNo (read-only)Yes, through setState
Does a change re-renderYesYes

Why must props be read-only? Because a render function is supposed to be pure (see #293): the same props render the same UI. Changing props means changing the input, and the parent will overwrite it on its next render — now you have two sources of truth and nobody can say which one to trust.

How to decide which one you need — a practical test:

  • You can compute it from props or other state → store neither, just compute it (derived data)
  • Only this component cares, and it changes → state
  • Several components need it → lift it to their common parent (#345)
  • The whole tree reads it and it rarely changes → Context

Follow-up: “Can you put props into state?” — you can, and it is usually a bug: useState(props.value) reads the value on the first render only, so later prop changes never reach the state. Do it only when you need an editable initial value, and think through whether a prop change should reset it. The edit feature in the real Q1 question is that scenario — it syncs explicitly with a useEffect.

React 与生态React & ecosystem#328

组件之间怎么通信

Communication between components

看答案Show answer

一句话:五种,按「距离」从近到远选。

  • 父 → 子:props。
  • 子 → 父: 父把回调函数当 props 传下去, 子调用它。「事件往上报」就是这个。
  • 兄弟之间状态提升到共同父级, 再分别往下传(#345)。
  • 跨很多层Context—— 适合主题、当前用户、语言这种 「整棵树都要读、又不常变」的值。
  • 全局、复杂、多处修改: 状态库(Redux / Zustand / Jotai) 或服务端状态库(TanStack Query)。

还有两个偏门但会问的:ref +useImperativeHandle(父组件主动调子组件的方法, 比如 focus()play()); 以及 props.children(组合优于配置,也是解决 props drilling 的一招)。

会追问:「什么时候该上状态库?」—— 判据:同一份状态被很多不相关的组件读写、 或者需要时间旅行调试 / 中间件只是「层数深」不该上 Redux, Context 或组合就够—— 这个回答比「大项目就用 Redux」好得多。

In one line: five ways, and you pick by distance — nearest first.

  • Parent → child: props.
  • Child → parent: the parent passes a callback down as a prop and the child calls it. That is all “events go up” means.
  • Between siblings: lift the state to their common parent and pass it back down to each one (#345).
  • Across many levels: Context — good for theme, current user, language: values the whole tree reads and that rarely change.
  • Global, complex, written from many places: a state library (Redux / Zustand / Jotai) or a server-state library (TanStack Query).

Two less common ones they still ask about: ref plus useImperativeHandle (the parent calls a method on the child, say focus() or play()); and props.children (composition over configuration, which is also one answer to props drilling).

Follow-up: “When should you reach for a state library?” — the test: one piece of state is read and written by many unrelated components, or you need time-travel debugging or middleware. Depth alone is no reason for Redux — Context or composition is enough — that answer beats “big project, use Redux” by a mile.

JSX两种最常用的方式示意Illustrative
1// 子 -> 父:传回调下去
2function Parent() {
3 const [text, setText] = useState("");
4 return <Child onChange={setText} />; // 父给回调
5}
6function Child({ onChange }) {
7 return <input onChange={(e) => onChange(e.target.value)} />;
8}
9
10// 用 children 组合,避免中间层被迫透传
11<Layout sidebar={<Nav />}>
12 <Article /> {/* Layout 不需要知道 Article 要什么 props */}
13</Layout>
1// Child to parent: pass a callback down
2function Parent() {
3 const [text, setText] = useState("");
4 return <Child onChange={setText} />; // the parent supplies the callback
5}
6function Child({ onChange }) {
7 return <input onChange={(e) => onChange(e.target.value)} />;
8}
9
10// Compose with children, so the middle layer is not forced to pass things through
11<Layout sidebar={<Nav />}>
12 <Article /> {/* Layout does not need to know what props Article wants */}
13</Layout>
React 与生态React & ecosystem#329

受控组件 vs 非受控组件

Controlled component vs uncontrolled component

看答案Show answer

一句话:受控 = 值由 React state 说了算value + onChange);非受控 = 值由 DOM 自己保管, 需要时用 ref 去读。

受控非受控
值放哪React stateDOM 节点
怎么读直接读 stateref.current.value
每次输入重渲染不会
实时校验 / 联动容易
代码量

默认用受控。因为一旦要做「输入为空就禁用提交按钮」 「实时显示字数」「两个字段联动」, 受控是唯一顺手的方式 ——Q1 那道真题的校验要求就是这样

非受控的两个真实场景:<input type="file">只能非受控,出于安全 JS 不能设它的值); 以及性能敏感的大表单 (每次按键都重渲染整个表单时)。

会追问(高频):value 传了但没传onChange 会怎样?」—— 输入框变成只读, 打字没反应,React 还会警告。
「怎么给受控组件一个初始值又不锁死?」——defaultValue,但那就是非受控了。
value={undefined} 呢?」—— React 会把它当成非受控,然后在你后来传了值时报「从非受控变成受控」的警告。 所以初始值该写 "" 而不是undefined

In one line: controlled = React state owns the value (value + onChange); uncontrolled = the DOM keeps the value and you read it with a ref when you need it.

ControlledUncontrolled
Where the value livesReact stateThe DOM node
How you read itStraight from stateref.current.value
Re-renders on every keystrokeYesNo
Live validation / linked fieldsEasyHard
Amount of codeMoreLess

Default to controlled. The moment you need “disable submit while the field is empty”, a live character count, or two fields that react to each other, controlled is the only comfortable option — the validation requirement in the real Q1 question works exactly that way.

Two real cases for uncontrolled: <input type="file"> (it can only be uncontrolled — for security reasons JS cannot set its value); and performance-sensitive large forms, where every keystroke would otherwise re-render the whole form.

Follow-ups — these come up a lot: “What happens if you pass value but no onChange?” — the input goes read-only, typing does nothing, and React warns you.
“How do you give a controlled component an initial value without locking it?” — defaultValue, but that makes it uncontrolled.
“And value={undefined}?” — React treats it as uncontrolled, then warns you about switching from uncontrolled to controlled once you do pass a value. So the initial value should be "", not undefined.

JSX两种写法示意Illustrative
1// 受控:闭环 value -> onChange -> setState -> value
2const [text, setText] = useState(""); // 注意初始值是 "",不是 undefined
3<input value={text} onChange={(e) => setText(e.target.value)} />
4<button disabled={text.trim() === ""}>提交</button> {/* 校验只有受控才顺手 */}
5
6// 非受控:值在 DOM 里
7const ref = useRef(null);
8<input ref={ref} defaultValue="初始" />
9<button onClick={() => console.log(ref.current.value)}></button>
1// Controlled: a closed loop of value -> onChange -> setState -> value
2const [text, setText] = useState(""); // note the initial value is "", not undefined
3<input value={text} onChange={(e) => setText(e.target.value)} />
4<button disabled={text.trim() === ""}>Submit</button> {/* validation is easy only when controlled */}
5
6// Uncontrolled: the value lives in the DOM
7const ref = useRef(null);
8<input ref={ref} defaultValue="initial" />
9<button onClick={() => console.log(ref.current.value)}>Read</button>
React 与生态React & ecosystem#345

什么是状态提升

What is Lifting State Up in React

看答案Show answer

一句话:两个兄弟组件要共享同一份数据时,把 state 移到它们最近的共同父级, 再通过 props 往下传、通过回调往上报。

为什么必须这样?因为 React 是单向数据流—— 数据只能往下流,兄弟之间没有通道。唯一的共享点就是共同祖先。

做法三步:① 找到最近的共同父级; ② state 搬上去; ③ 父级把「值」和「改值的函数」 分别传给两个孩子。

代价(要主动说):提得越高,中间层被迫透传的 props 越多(这就是 props drilling,#331), 而且父级重渲染会带着整棵子树重渲染。所以原则是「提到刚好够用的那一层,别更高」。

Q1 那道真题就是一个标准的状态提升notes 放在NoteManager(父), 表单和表格都是它的孩子; 表单提交时调用父传下来的回调。

会追问:「提太高怎么办?」—— Context、组合(children)、 或者状态库。注意 Context 解决的是「传递」, 不是「共享」—— state 该放哪还是得想清楚。

In one line: when two sibling components need the same data, move the state up to their closest common parent, then pass it down through props and report back through callbacks.

Why does it have to work this way? Because React has one-way data flow — data only travels down, and siblings have no channel between them. The only shared point is a common ancestor.

Three steps: (1) find the closest common parent; (2) move the state up there; (3) have the parent hand the value and the function that changes it to each child.

The cost — bring it up yourself: the higher you lift, the more props the middle layers are forced to pass through (that is props drilling, #331), and a parent re-render drags the whole subtree with it. So the rule is: lift to the lowest layer that works, and no higher.

The real Q1 question is a textbook lift: notes lives in NoteManager (the parent), and both the form and the table are its children; on submit the form calls the callback the parent passed down.

Follow-up: “What if it is lifted too high?” — Context, composition (children), or a state library. Note that Context solves delivery, not sharing — you still have to decide where the state belongs.

React 与生态React & ecosystem#331

什么是 props drilling

What is props drilling

看答案Show answer

一句话:为了把数据送到深处的组件,中间那些根本不用它的组件被迫一层层往下传

为什么是问题:

  • 中间组件被污染—— 它的 props 签名里出现了跟它无关的字段, 可复用性下降。
  • 改一处动一串—— 加一个字段要改路径上每一个组件。
  • 额外重渲染—— 值变了整条路径都重渲染。

四种解法,按代价从小到大:

  1. 组合 / children——最被低估的一招。 把元素直接当 props 传下去, 中间层就不用知道它需要什么数据。
  2. Context—— 适合主题、用户、语言这类 「整棵树都读、不常变」的值。我们那道主题切换变式题就是这个。
  3. 状态库—— 复杂全局状态。
  4. 把组件树重新拆一下—— 有时 drilling 只是拆分方式不合理的症状。

会追问:「传两三层也要上 Context 吗?」——不要。 两三层的 props 是清晰的、可追踪的; Context 会让「这个值从哪来」变得不明显, 而且 context 一变所有消费者都重渲染「drilling 超过三四层且中间层完全无关」才值得上。

In one line: to get data down to a deep component, the components in between that have no use for it are forced to pass it along, level by level.

Why that is a problem:

  • The middle components get polluted — fields that have nothing to do with them show up in their props signature, so they get less reusable.
  • One change touches a whole chain — adding a field means editing every component on the path.
  • Extra re-renders — when the value changes, the entire path re-renders.

Four fixes, cheapest first:

  1. Composition / children the most underrated one. Pass elements down as props and the middle layers never need to know what data they want.
  2. Context — good for theme, user, language: values the whole tree reads and that rarely change. Our theme-switching variant question is this one.
  3. A state library — for complex global state.
  4. Re-splitting the component tree — sometimes drilling is just a symptom of a bad split.

Follow-up: “Do two or three levels need Context?” — no. Two or three levels of props are clear and easy to trace; Context makes “where did this value come from” invisible, and every consumer re-renders when the context changes. It pays off once drilling passes three or four levels and the middle layers are completely unrelated.

React 与生态React & ecosystem#336

什么是 Pure Component

What are Pure Component

看答案Show answer

一句话:React.PureComponent自带一个浅比较shouldComponentUpdate—— props 和 state 浅比较都没变就跳过渲染。 函数组件的对应物是React.memo

「浅比较」是本题全部的考点。它对每个 prop 用Object.is 比一层。 所以:

  • 传对象/数组/函数字面量 → 优化完全失效, 因为每次渲染都是新引用。
  • 内部深层改了对象 → 不会重渲染, 因为引用没变 —— 界面就不更新了。

所以 PureComponent /memo 和「不可变更新」是一对: 你必须每次造新对象, 浅比较才能正确地判断出「变了」。反过来,如果你就地改对象, 加了 memo 反而会制造 bug。

会追问:「怎么让 memo 真正生效?」—— 对象和数组用 useMemo、 函数用 useCallback稳住引用。三个必须配套用, 只加 memo 往往一点用没有 (见 #346)。
「是不是所有组件都该 memo?」—— 不是。浅比较本身也有成本,props 多且经常变的组件, 加了反而更慢

In one line: React.PureComponent ships with a shallow-comparison shouldComponentUpdate — if a shallow compare of props and state finds nothing changed, it skips the render. The function-component equivalent is React.memo.

The shallow compare is the whole point of this question. It runs Object.is on each prop, one level deep. So:

  • Pass an object, array or function literal and the optimisation is dead, because every render creates a new reference.
  • Mutate something deep inside an object and it will not re-render, because the reference never changed — and the UI just stops updating.

So PureComponent / memo and immutable updates come as a pair: you have to build a new object every time for the shallow compare to see a change. The reverse holds too — mutate in place and adding memo manufactures bugs.

Follow-up: “How do you make memo actually work?” — stabilise the references: useMemo for objects and arrays, useCallback for functions. The three go together; memo on its own often does nothing at all (see #346).
“Should every component be memoised?” — no. The shallow compare costs something too, so a component with many frequently changing props gets slower, not faster.

JSXmemo 生效的前提示意Illustrative
1const Row = React.memo(function Row({ item, onPick }) {});
2
3// ✗ memo 白加:两个 prop 每次都是新引用
4<Row item={{ ...raw }} onPick={() => pick(raw.id)} />
5
6// ✓ 稳住引用,memo 才有意义
7const item = useMemo(() => ({ ...raw }), [raw]);
8const onPick = useCallback((id) => pick(id), [pick]);
9<Row item={item} onPick={onPick} />
1const Row = React.memo(function Row({ item, onPick }) {});
2
3// ✗ memo achieves nothing: both props are a new reference every render
4<Row item={{ ...raw }} onPick={() => pick(raw.id)} />
5
6// ✓ keep the references stable, and memo starts to mean something
7const item = useMemo(() => ({ ...raw }), [raw]);
8const onPick = useCallback((id) => pick(id), [pick]);
9<Row item={item} onPick={onPick} />
React 与生态React & ecosystem#338

什么是 Fragment

React Fragment

看答案Show answer

一句话:一个不产生真实 DOM 节点的容器, 用来满足「JSX 必须有单一根节点」的要求, 同时不往页面里多塞一层div

写法两种:<React.Fragment>和简写 <></>

为什么需要它(举得出场景才算答好):

  • 表格——<tr> 里套一层div非法 HTML, 浏览器会把它挪出去,布局直接坏。
  • Flex / Grid 布局—— 多一层 div打断父容器和子项的直接关系flex 属性全失效。
  • 减少 DOM 层数, 样式选择器也更好写。

会追问:「Fragment 上能加 key 吗?」——能,但必须用完整写法<React.Fragment key={id}>, 简写 <> 不支持任何属性。在列表里渲染多个兄弟元素时就得这么写, 这是简写唯一的限制。

In one line: a container that produces no real DOM node, so you can satisfy “JSX needs a single root” without pushing another div into the page.

Two forms: <React.Fragment> and the shorthand <></>.

Why you need it — you only answer this well with concrete cases:

  • Tables — a div inside a <tr> is invalid HTML; the browser hoists it out and the layout breaks on the spot.
  • Flex / Grid layouts — an extra div breaks the direct relationship between the container and its items, so every flex property stops working.
  • Fewer DOM levels, and easier style selectors.

Follow-up: “Can a Fragment take a key?” — yes, but only in the long form <React.Fragment key={id}>; the shorthand <> accepts no attributes at all. You need this whenever a list renders several sibling elements per item, and it is the shorthand’s only limitation.

JSXFragment 的两个真实场景示意Illustrative
1// ✗ tr 里多一层 div:非法 HTML,布局会坏
2<tr><div><td>A</td><td>B</td></div></tr>
3
4// ✓
5<tr><><td>A</td><td>B</td></></tr>
6
7// 列表里要 key,就不能用简写
8{rows.map((r) => (
9 <React.Fragment key={r.id}>
10 <dt>{r.term}</dt>
11 <dd>{r.desc}</dd>
12 </React.Fragment>
13))}
1// ✗ an extra div inside tr: invalid HTML, and the layout breaks
2<tr><div><td>A</td><td>B</td></div></tr>
3
4// ✓
5<tr><><td>A</td><td>B</td></></tr>
6
7// A list needs a key, so the short form cannot be used
8{rows.map((r) => (
9 <React.Fragment key={r.id}>
10 <dt>{r.term}</dt>
11 <dd>{r.desc}</dd>
12 </React.Fragment>
13))}
React 与生态React & ecosystem#335

什么是 HOC

What is HOC

看答案Show answer

一句话:高阶组件 ——接收一个组件、返回一个增强后的新组件的函数。它是「复用组件逻辑」的老方案。

withRouterconnect(Redux)、withStyles 都是 HOC。 本质就是 #292 的高阶函数用在组件上。

三个必须注意的点(考点在这):

  • 要透传 props——<Comp {...props} />, 不然把原来的 props 吞了。
  • 要拷贝静态方法—— 包一层之后原组件的静态属性丢了 (hoist-non-react-statics 干这事)。
  • ref 传不进去—— 要用 forwardRef

为什么现在少用了(这才是重点):

  • wrapper 地狱—— 叠三四层之后 DevTools 里全是嵌套, 难调试。
  • props 来源不明—— 组件里出现一个 user prop, 你不知道是哪个 HOC 注进来的。
  • 命名冲突—— 两个 HOC 都注入 data 就打架了。

自定义 hook 解决了全部三条: 平铺、来源显式 (const user = useUser()一眼看出来)、 名字由你决定。所以现在优先写 hook。

会追问:「那 HOC 还有用吗?」—— 有两个 hook 替代不了的场合:需要「包裹」渲染结果(比如给所有页面套一层错误边界或布局)、 以及要改写 props 后再传给一个你无法修改的组件

In one line: a higher-order component — a function that takes a component and returns an enhanced one. It is the old answer to reusing component logic.

withRouter, connect (Redux) and withStyles are all HOCs. It is nothing more than the higher-order function from #292 applied to components.

Three things you must get right — the marks are here:

  • Forward the props <Comp {...props} />, or you swallow the ones the component already had.
  • Copy the statics — wrapping loses the original component’s static properties (hoist-non-react-statics exists for this).
  • Refs do not pass through — you need forwardRef.

Why it fell out of favour — this is the real point:

  • Wrapper hell — stack three or four and DevTools is nothing but nesting; debugging hurts.
  • Props of unknown origin — a user prop shows up in the component and you cannot tell which HOC injected it.
  • Name collisions — two HOCs both injecting data fight each other.

Custom hooks fix all three: they stay flat, the origin is explicit (const user = useUser() says it out loud), and you choose the name. So hooks come first now.

Follow-up: “Is there still a use for HOCs?” — two places hooks cannot cover: when you need to wrap the rendered output (putting an error boundary or a layout around every page), and when you have to rewrite props before handing them to a component you cannot modify.

JSXHOC 与 hook 的对比示意Illustrative
1// HOC
2function withAuth(Comp) {
3 return function Wrapped(props) {
4 const user = useUser();
5 if (!user) return <Login />;
6 return <Comp {...props} user={user} />; // 记得透传 props
7 };
8}
9
10// 同一件事用 hook:平的,而且来源一眼看得出
11function Page() {
12 const user = useUser(); // ← 明确知道 user 从哪来
13 if (!user) return <Login />;
14 return <Content user={user} />;
15}
1// HOC
2function withAuth(Comp) {
3 return function Wrapped(props) {
4 const user = useUser();
5 if (!user) return <Login />;
6 return <Comp {...props} user={user} />; // remember to pass props through
7 };
8}
9
10// The same thing with a hook: flat, and you can see where the value came from
11function Page() {
12 const user = useUser(); // ← it is obvious where user comes from
13 if (!user) return <Login />;
14 return <Content user={user} />;
15}
React 与生态React & ecosystem#324

什么是 hooks,为什么要用

What are hooks in React and Why do we use them

看答案Show answer

一句话: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}
React 与生态React & ecosystem#339

useMemo vs useCallback

useMemo vs useCallback

看答案Show answer

一句话: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
React 与生态React & ecosystem#346

React.memo vs useMemo

React.memo vs useMemo

看答案Show answer

一句话: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.

这些题从哪来Where these come from

99 道来自面试题库 #269–#387;TypeScript 深度那 6 道是 DrillLab 自出的(senior 补强,卡片上有标注)。答案都是 DrillLab 写的,所以讲解里的代码块一律标「示意」。每道题都能点回它出处的那一节课。99 questions come from the interview bank (#269–#387); the 6 TypeScript deep-dive ones are DrillLab-made (marked on the card). All answers are written by DrillLab, so every code block here is labelled “demo”. Each card links back to the lesson it came from.