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 道 · 第 7 / 9 页。105 of 105 questions · page 7 / 9.
React 与生态React & ecosystem#340

自定义 hook 是干什么的,命名有什么约定

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

看答案Show answer

一句话:「带状态的逻辑」抽出来复用。 命名必须以 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
React 与生态React & ecosystem#343

怎么优化 React 性能

How could you improve performance in React

看答案Show answer

先说这一句,再列手段:「先用 React DevTools Profiler 找出到底哪个组件渲染慢、渲染了多少次, 再决定动哪。」上来就背 useMemo会显得像背题。

手段分三类:

① 少渲染

  • React.memo +useMemo +useCallback三件套配套用(#346)
  • state 下移—— 把频繁变的 state 放到真正需要它的那个小组件里, 别提到顶层带着整棵树重渲染。这招常常比加 memo 有效得多。
  • children 组合—— 父组件重渲染时, 作为 prop 传进来的 children不会重建
  • 拆分 Context —— 一个 context 里放太多东西, 改任何一项所有消费者都重渲染。

② 少下载

  • 代码分割——React.lazy +Suspense,按路由切(#347)
  • 按需引入第三方库,别import _ from "lodash"
  • 用 bundle analyzer 看谁占体积

③ 少算 / 少画

  • 长列表虚拟化—— 只渲染视口内的几十行。一万行的列表,这一条比其他所有优化加起来都有用。
  • 列表 key 用稳定 id(#330)
  • 输入防抖、搜索节流
  • React 18 的 useTransition /useDeferredValue—— 让重活不挡住输入

会追问:「怎么知道有没有多余渲染?」—— Profiler 的「Highlight updates」 或者 <Profiler onRender>; 以及注意 StrictMode 下开发模式会渲染两次, 别把它当成 bug。

Open with this sentence, then list the techniques: “First I use the React DevTools Profiler to find which component is slow and how many times it renders, then I decide what to touch.” Reciting useMemo straight away sounds like you memorised an answer sheet.

Three families of technique:

① Render less

  • React.memo + useMemo + useCallback used as a set (#346)
  • Push state down — put frequently changing state in the small component that actually needs it instead of lifting it to the top and re-rendering the whole tree. This often helps far more than adding memo.
  • Compose with children — when the parent re-renders, children passed in as a prop are not rebuilt.
  • Split your contexts — put too much in one context and changing any field re-renders every consumer.

② Download less

  • Code splittingReact.lazy + Suspense, split per route (#347)
  • Import third-party libraries piecemeal, not import _ from "lodash"
  • Run a bundle analyzer to see who is taking up the space

③ Compute less, paint less

  • Virtualise long lists — render only the few dozen rows in the viewport. On a ten-thousand-row list this beats every other optimisation put together.
  • Use stable ids as list keys (#330)
  • Debounce input, throttle search
  • React 18’s useTransition / useDeferredValue — keep the heavy work from blocking typing

Follow-up: “How do you know there are wasted renders?” — the Profiler’s “Highlight updates”, or <Profiler onRender>; and remember StrictMode renders twice in development, so do not mistake that for a bug.

React 与生态React & ecosystem#342

React 里怎么写样式

How to use styles in React

看答案Show answer

一句话:五种, 各有明确的取舍。

方式好处代价
普通 CSS / SCSS 文件零成本、能用全部 CSS 特性类名全局,会冲突
CSS Modules类名自动加哈希,天然隔离动态样式要配 CSS 变量
行内 style动态值最直接没有伪类、媒体查询、动画; 每次渲染新对象
CSS-in-JS(styled-components)能用 props 决定样式,作用域天然隔离运行时开销,SSR 要额外配置
原子化(Tailwind)不用起类名,产物体积可控JSX 里类名很长,团队要统一约定

「动态样式」的推荐做法—— 这是加分点:用 CSS 变量而不是行内 style。 把变量写在行内, 真正的样式规则还在 CSS 文件里 —— 这样既能动态,又保留伪类和媒体查询。本站的深色模式就是这么做的(切 data-theme 属性, CSS 变量整套换)。

会追问:「行内 style 为什么影响性能?」—— 每次渲染都创建新对象, 会破坏子组件的 memo; 而且它不能被浏览器按规则缓存。要用就 useMemo 稳住。

In one line: five ways, each with a clear trade-off.

ApproachUpsideCost
Plain CSS / SCSS filesFree, and every CSS feature is availableClass names are global, so they collide
CSS ModulesHashed class names, isolated by defaultDynamic styles need CSS variables
Inline styleThe most direct way to use a dynamic valueNo pseudo-classes, media queries or animations; a new object every render
CSS-in-JS (styled-components)props can drive the styles, and scoping needs no extra workRuntime cost, and SSR needs extra setup
Atomic (Tailwind)No naming, and the output size stays under controlVery long class strings in JSX, and the team needs conventions

The recommended way to do dynamic styles — this is the bonus point: use a CSS variable, not an inline style. Put only the variable inline and leave the actual rule in the CSS file — you get the dynamic value and keep pseudo-classes and media queries. That is how this site’s dark mode works (flip the data-theme attribute and the whole set of CSS variables swaps).

Follow-up: “Why do inline styles hurt performance?” — every render creates a new object, which breaks memo on the child, and the browser cannot cache it as a rule. If you must use one, stabilise it with useMemo.

JSX动态样式的正确做法示意Illustrative
1// 推荐:行内只放变量,规则留在 CSS 里
2<div className="bar" style={{ "--pct": `${percent}%` }} />
3
4/* CSS 里 */
5.bar::after { width: var(--pct); } /* 伪类照样能用 */
6@media (max-width: 480px) { .bar { height: 4px; } }
7
8// ✗ 行内写全套:没法写伪类和媒体查询,还每次新对象
9<div style={{ width: `${percent}%`, background: "#2b6" }} />
1// Recommended: only the variable goes inline, the rules stay in CSS
2<div className="bar" style={{ "--pct": `${percent}%` }} />
3
4/* In the CSS */
5.bar::after { width: var(--pct); } /* pseudo-classes still work */
6@media (max-width: 480px) { .bar { height: 4px; } }
7
8// ✗ everything inline: no pseudo-classes, no media queries, and a new object each time
9<div style={{ width: `${percent}%`, background: "#2b6" }} />
React 与生态React & ecosystem#344

React 18 有哪些新变化

What are the new changes in react 18

看答案Show answer

一句话:核心是并发渲染(concurrent rendering)—— React 可以中断、暂停、恢复一次渲染, 好让高优先级的更新先跑。

五个具体变化(挑三四个说清就够):

  • 自动批处理(automatic batching)—— React 17 只在 React 事件回调里批处理,setTimeout、 Promise、原生事件里的多次setState 会各触发一次渲染。18 里全都批处理了。这是最容易被观察到的变化。
  • useTransition—— 把一个更新标记为「不着急」。 典型场景:输入框旁边有个很重的搜索结果列表,输入保持流畅,列表慢慢跟上
  • useDeferredValue—— 同一个目的, 但是从「值」的角度:给我一个滞后版本的值。
  • 新的 root API——createRoot 取代ReactDOM.render不换它就用不上任何并发特性
  • Suspense 支持 SSR—— 流式渲染、选择性 hydration。
    另外 useId(服务端客户端一致的 id)、useSyncExternalStore(给状态库用的)。

会追问:「StrictMode 在 18 里有什么变化?」——会额外「挂载 → 卸载 → 再挂载」一次, 所以 effect 的清理函数会被执行。这是为了提前暴露「没写清理函数」的组件(见 #332),为将来的 Offscreen 特性做准备。
「升级要注意什么?」—— 换 createRoot; 自动批处理可能让依赖「每次 setState 都立刻渲染」 的老代码行为改变 (必要时用 flushSync 逃出批处理)。

In one line: the core is concurrent rendering — React can interrupt, pause and resume a render so a higher-priority update goes first.

Five concrete changes — covering three or four properly is enough:

  • Automatic batching — React 17 only batched inside React event handlers, so several setState calls in a setTimeout, a Promise or a native event each triggered their own render. 18 batches all of them. This is the change you notice most easily.
  • useTransition — mark an update as “not urgent”. The classic case is an input next to a heavy list of search results: typing stays smooth and the list catches up.
  • useDeferredValue — same goal, from the value side: give me a version of this value that lags behind.
  • The new root APIcreateRoot replaces ReactDOM.render, and until you switch you get none of the concurrent features.
  • Suspense works on the server — streaming and selective hydration.
    Plus useId (ids that match between server and client) and useSyncExternalStore (for state libraries).

Follow-up: “What changed for StrictMode in 18?” — it now does an extra mount, unmount, mount, so effect cleanup functions actually run. The point is to expose components that forgot to write cleanup (see #332) and to prepare for the future Offscreen feature.
“What do you watch out for when upgrading?” — switch to createRoot; automatic batching can change the behaviour of old code that assumed “every setState renders right away” (use flushSync to escape a batch when you truly need to).

JSX两个最能感知的变化示意Illustrative
1// React 17:这里会渲染两次
2setTimeout(() => {
3 setA(1);
4 setB(2);
5}, 0);
6// React 18:只渲染一次(自动批处理)
7
8// useTransition:输入不卡,列表慢慢跟
9const [isPending, startTransition] = useTransition();
10
11function onChange(e) {
12 setQuery(e.target.value); // 紧急:输入框马上响应
13 startTransition(() => setList(filter(e.target.value))); // 不急
14}
15{isPending && <span>更新中…</span>}
1// React 17: this renders twice
2setTimeout(() => {
3 setA(1);
4 setB(2);
5}, 0);
6// React 18: renders once (automatic batching)
7
8// useTransition: the input stays responsive and the list catches up
9const [isPending, startTransition] = useTransition();
10
11function onChange(e) {
12 setQuery(e.target.value); // urgent: the input responds at once
13 startTransition(() => setList(filter(e.target.value))); // not urgent
14}
15{isPending && <span>Updating</span>}
React 与生态React & ecosystem#347

React.lazy 是干什么的

What is React lazy function

看答案Show answer

一句话:让组件按需加载—— 打包时被切成单独的 chunk, 真正渲染到它的时候才下载。 必须配 Suspense 给个占位。

为什么需要:SPA 默认把全站打成一个包, 首屏要下载所有页面的代码 (见 #321)。按路由切分是收益最大的一刀 —— 用户打开首页不该下载「设置页」的代码。

四个注意点:

  • lazy 的参数必须返回一个default 导出的 Promise —— 所以配的是 default export; 具名导出要自己包一层。
  • 动态 import()的路径不能是完全动态的变量, 打包工具需要在编译期能分析出来。
  • 加载失败要有兜底—— 网络断了 chunk 拉不下来, 要用错误边界包住(见 #333)。这一点很多人不提,是加分项。
  • 别切太碎 —— 每个 chunk 都是一次请求。

会追问:「怎么避免切换页面时闪一下 loading?」——预加载:鼠标悬停在链接上时 就调一次那个 import()(模块会被缓存)。 或者用 React 18 的useTransition让旧页面留在屏幕上直到新页面就绪。

In one line: it makes a component load on demand — the bundler cuts it into its own chunk, and the chunk is fetched only when you actually render it. It needs a Suspense around it to supply a placeholder.

Why you need it: by default an SPA is one bundle, so the first screen downloads the code for every page (see #321). Splitting per route is the highest-value cut — opening the home page should not download the settings page.

Four things to watch:

  • What you pass to lazy must return a Promise with a default export — so it pairs with default export; a named export needs a small wrapper.
  • The path in a dynamic import() cannot be a fully dynamic variable — the bundler has to be able to analyse it at build time.
  • Handle the failure case — if the network drops the chunk never arrives, so wrap it in an error boundary (see #333). Most people leave this out, so it is a bonus point.
  • Do not split too finely — every chunk is another request.

Follow-up: “How do you avoid a loading flash when switching pages?” — preload: call that import() once when the mouse hovers the link (the module gets cached). Or use React 18’s useTransition to keep the old page on screen until the new one is ready.

JSXlazy 的完整用法(含失败兜底)示意Illustrative
1const Settings = lazy(() => import("./pages/Settings"));
2
3<ErrorBoundary fallback={<p>加载失败,请刷新</p>}>
4 <Suspense fallback={<Spinner />}>
5 <Settings />
6 </Suspense>
7</ErrorBoundary>
8
9// 预加载:悬停时就开始下载
10const preload = () => import("./pages/Settings");
11<Link to="/settings" onMouseEnter={preload}>设置</Link>
1const Settings = lazy(() => import("./pages/Settings"));
2
3<ErrorBoundary fallback={<p>Loading failed, please refresh</p>}>
4 <Suspense fallback={<Spinner />}>
5 <Settings />
6 </Suspense>
7</ErrorBoundary>
8
9// Preloading: start downloading on hover
10const preload = () => import("./pages/Settings");
11<Link to="/settings" onMouseEnter={preload}>Settings</Link>
React 与生态React & ecosystem#332

什么是 StrictMode

What is React strict mode

看答案Show answer

一句话:一个只在开发模式生效的检查工具组件, 通过故意多做一次来暴露不安全的写法。生产构建里它什么都不做。

它做三件事:

  • 渲染函数调用两次—— 暴露渲染过程中的副作用 (改了外部变量、直接 mutate props)。因为 render 阶段可能被中断重跑 (见 #353),所以它必须是纯的。
  • React 18 起:effect 会 「挂载 → 卸载 → 再挂载」——专门暴露「没写清理函数」的 effect。 如果你的组件挂两次就出问题 (重复订阅、定时器翻倍), 那就是真 bug。
  • 警告废弃 API 和过时的 ref 写法。

最重要的一句: 「双重渲染导致的问题」不是 StrictMode 的问题, 是你的代码的问题。很多人第一反应是「关掉它」—— 正确反应是修好副作用。

会追问:「日志被打印两次正常吗?」—— 开发模式下正常,因为渲染函数被调了两次; 生产不会。
「请求被发两次呢?」——这个要认真看: 说明你的 effect 没有正确处理清理 —— 虽然对幂等的 GET 无害, 但它同时提示你「竞态防护写了没有」。我们那道 fetch 变式题里ignore 标志 +abort 的写法, 正好让它在 StrictMode 下也表现正确。

In one line: a checking component that only does anything in development, and it exposes unsafe code by deliberately doing things twice. In a production build it does nothing.

It does three things:

  • Calls your render function twice — which surfaces side effects during render (mutating an outer variable, mutating props directly). Because the render phase can be interrupted and re-run (see #353), it has to be pure.
  • Since React 18: effects run mount, unmount, mountspecifically to expose effects with no cleanup function. If mounting twice breaks your component (a duplicated subscription, a doubled timer), that is a real bug.
  • Warns about deprecated APIs and legacy ref patterns.

The most important sentence: a problem caused by double rendering is not StrictMode’s problem, it is your code’s. Most people’s first instinct is to turn it off — the right instinct is to fix the side effect.

Follow-up: “Is it normal for my log to print twice?” — yes in development, because the render function ran twice; it will not in production.
“What about my request firing twice?” — take that one seriously: it means your effect is not cleaning up properly. Harmless for an idempotent GET, but it is also telling you to ask whether you guarded against races. The ignore flag plus abort pattern from our fetch variant is exactly what makes it behave correctly under StrictMode too.

React 与生态React & ecosystem#333

什么是错误边界,有什么用

What are error boundaries and How are they useful

看答案Show answer

一句话:一个能捕获子树渲染错误的类组件, 出错时渲染兜底 UI,而不是让整个应用白屏

怎么写:实现 getDerivedStateFromError(用来切换到兜底 UI)和componentDidCatch(用来上报日志)。目前必须是 class 组件—— 没有对应的 hook。

抓不到什么(这才是考点):

  • 事件处理函数里的错误—— 因为那不在渲染过程中。 要自己 try/catch
  • 异步代码里的错误——setTimeout、 Promise 回调(和 #310 同一个道理)。
  • 服务端渲染
  • 错误边界自己抛的错。

放哪:粒度要合适。只在根节点放一个, 一个小组件挂了整页还是没了;推荐按「独立区块」放—— 每个路由页面、 每个可独立失败的挂件 (侧栏、图表、评论区)。 这样一块坏了其他还能用。

会追问:「怎么让用户能恢复?」—— 兜底 UI 里给一个「重试」按钮, 把边界的 state 重置 (或者换一个 key强制重建子树)。react-error-boundary这个库提供了 resetErrorBoundary

In one line: a class component that catches render errors in its subtree and renders a fallback UI instead of letting the whole app go blank.

How you write one: implement getDerivedStateFromError (to switch to the fallback) and componentDidCatch (to report the error). It still has to be a class component — there is no hook equivalent.

What it does not catch — this is the real question:

  • Errors inside event handlers — those do not happen during render. Use your own try/catch.
  • Errors in async codesetTimeout, Promise callbacks (the same reason as #310).
  • Server-side rendering.
  • Errors thrown by the boundary itself.

Where to put them: get the granularity right. One at the root only means a single broken widget still wipes out the page; put one around each independent block — each route, and each widget that can fail on its own (sidebar, chart, comments). Then one broken block leaves the rest usable.

Follow-up: “How does the user recover?” — give the fallback a “Retry” button that resets the boundary’s state (or changes a key to force the subtree to rebuild). The react-error-boundary library gives you resetErrorBoundary for this.

JSX错误边界示意Illustrative
1class ErrorBoundary extends React.Component {
2 state = { error: null };
3
4 static getDerivedStateFromError(error) {
5 return { error }; // 切到兜底 UI
6 }
7
8 componentDidCatch(error, info) {
9 report(error, info.componentStack); // 上报
10 }
11
12 render() {
13 if (this.state.error) {
14 return (
15 <div>
16 <p>这块出错了</p>
17 <button onClick={() => this.setState({ error: null })}>重试</button>
18 </div>
19 );
20 }
21 return this.props.children;
22 }
23}
24
25// 按区块放,而不是只在根节点放一个
26<ErrorBoundary><Sidebar /></ErrorBoundary>
27<ErrorBoundary><Chart /></ErrorBoundary>
1class ErrorBoundary extends React.Component {
2 state = { error: null };
3
4 static getDerivedStateFromError(error) {
5 return { error }; // switch to the fallback UI
6 }
7
8 componentDidCatch(error, info) {
9 report(error, info.componentStack); // report it
10 }
11
12 render() {
13 if (this.state.error) {
14 return (
15 <div>
16 <p>Something went wrong in this part</p>
17 <button onClick={() => this.setState({ error: null })}>Retry</button>
18 </div>
19 );
20 }
21 return this.props.children;
22 }
23}
24
25// Put one per region rather than a single one at the root
26<ErrorBoundary><Sidebar /></ErrorBoundary>
27<ErrorBoundary><Chart /></ErrorBoundary>
React 与生态React & ecosystem#334

React Router 的意义是什么

React router, What is the point of it

看答案Show answer

一句话:让 SPA有「URL」这个概念—— 把地址栏和当前渲染哪个组件对应起来, 不刷新页面就能切换。

它解决四件事:

  • URL ↔ 组件的映射
  • 前进后退能用—— 接管 history API
  • 深链接可分享—— 发给别人能直接打开那个页面
  • 嵌套路由—— 布局层和内容层分开 (Outlet

核心 API:BrowserRouterRoutes / RouteLink /NavLinkuseNavigateuseParamsuseSearchParamsOutlet

三个实践要点:

  • Link 不要用<a>—— 后者会真的刷新整页, SPA 的意义就没了。
  • BrowserRouter 需要服务端 把所有路径都回退到index.html, 否则刷新子路由会 404。 没法配服务器就用HashRouter这题很常问。
  • React.lazy按路由切代码(#347)。

会追问:「路由守卫怎么做?」—— React Router 没有内置守卫, 自己写一个包装组件: 没登录就 <Navigate to="/login" />

In one line: it gives an SPA the concept of a URL — it maps the address bar to whichever component is rendered, and switches between them without a page reload.

It solves four things:

  • Mapping URLs to components
  • Back and forward work — it takes over the history API
  • Deep links are shareable — send one to someone and it opens that page directly
  • Nested routes — the layout layer and the content layer stay separate (Outlet)

The core API: BrowserRouter, Routes / Route, Link / NavLink, useNavigate, useParams, useSearchParams, Outlet.

Three practical points:

  • Use Link, not <a> — an anchor really does reload the whole page, which throws away the point of an SPA.
  • BrowserRouter needs the server to fall back to index.html for every path, otherwise refreshing a nested route 404s. If you cannot configure the server, use HashRouter. This one comes up a lot.
  • Pair it with React.lazy to split code per route (#347).

Follow-up: “How do you do route guards?” — React Router has none built in; you write a wrapper component yourself: if the user is not signed in, render <Navigate to="/login" />.

React 与生态React & ecosystem#348

写 React 时你会注意哪些最佳实践

When coding React, what are some best practices that you keep in mind

看答案Show answer

这是开放题,答「有取舍的清单」比列一堆规则好。按重要性给六条:

  • 不可变更新。永远造新对象/新数组, 不就地改 —— 否则 React 比引用发现不了变化,界面不更新。这是 React 里最常见的 bug 来源。
  • 能算出来的别放 state。派生数据当场算, 不要用 useEffect 同步两份数据 —— 同一个事实存两份必然会不一致。
  • state 放在「刚好够用」的那一层。提太高造成 drilling 和多余渲染, 提太低兄弟拿不到。
  • effect 里建立的东西一定要清理。定时器、监听器、订阅、在途请求。判别法:effect 里出现setInterval /addEventListener /subscribe /fetch,就一定要return
  • 列表 key 用稳定业务 id,不用 index。
  • 先测量再优化。别默认给所有东西套useMemo

再补两条工程上的:组件保持小而专一 (一个组件干一件事);用 TypeScript—— props 的形状是组件的契约, 写下来比靠记忆可靠。

如果面试官想听更具体的, 可以说: 「我会开着 eslint-plugin-react-hooks不用注释关掉exhaustive-deps 警告—— 它几乎每次都是对的, 想绕过它通常说明该重构了。」 这条很能体现实战经验。

This is an open question, and a list with trade-offs beats a pile of rules. Six, most important first:

  • Update immutably. Always build a new object or array, never edit in place — otherwise React compares references, sees nothing changed, and the UI does not update. This is the single most common source of bugs in React.
  • If you can compute it, do not store it in state. Derive it on the spot; do not use useEffect to keep two copies in sync — one fact stored twice will drift.
  • Keep state at the lowest level that works. Too high and you get prop drilling and wasted renders; too low and siblings cannot reach it.
  • Anything an effect sets up has to be torn down. Timers, listeners, subscriptions, in-flight requests. The test: if the effect contains setInterval, addEventListener, subscribe or fetch, it must return something.
  • Use stable business ids as list keys, not the index.
  • Measure before optimising. Do not wrap everything in useMemo by default.

Two more on the engineering side: keep components small and single-purpose (one component, one job); and use TypeScript — the shape of the props is the component’s contract, and writing it down beats remembering it.

If the interviewer wants something more specific, you can say: “I keep eslint-plugin-react-hooks on and I do not silence the exhaustive-deps warning with a comment — it is right almost every time, and wanting to get around it usually means the code needs restructuring.” That one really shows hands-on experience.

React 与生态React & ecosystem#349

Redux vs Context API

Redux vs Context API

看答案Show answer

一句话(这句最关键):它们解决的不是同一个问题。Context 是「传递」方案 (怎么把值送到深处), Redux 是「状态管理」方案 (状态怎么组织、怎么改、怎么调试)。

「Context 能不能替代 Redux」—— 严格说,Context + useReducer可以覆盖 Redux 的基本功能, 但缺三样东西:

  • 没有精细的订阅。context 一变,所有消费者都重渲染, 哪怕它只用了其中一个字段。 Redux 的 useSelector只在你选的那部分变化时重渲染。这是最大的实际差别。
  • 没有中间件。统一处理异步、日志、 持久化都要自己造。
  • 没有 DevTools。时间旅行、action 记录、 状态 diff 都没有。
ContextRedux
适合主题、当前用户、语言 ——不常变频繁变、多处读写、需要调试追溯
订阅粒度整个 value按 selector
额外依赖
样板代码有(Redux Toolkit 后少很多)

会追问:「现在还用 Redux 吗?」—— 答得诚实一点:纯客户端状态很多项目改用 Zustand / Jotai(更轻);服务端数据用 TanStack Query / SWR(缓存、去重、重试是它们的本职, Redux 做这个是硬凑)。Redux 现在的强项是「复杂的、 有大量交互逻辑的客户端状态 + 要可追溯调试」, 而且一定要用 Redux Toolkit 而不是手写。

In one line — and this is the key sentence: they do not solve the same problem. Context is a delivery mechanism (how a value reaches something deep in the tree); Redux is a state management solution (how state is organised, changed and debugged).

“Can Context replace Redux?” — strictly speaking, Context plus useReducer covers Redux’s basic features, but three things are missing:

  • No fine-grained subscription. When the context changes, every consumer re-renders, even one that only reads a single field. Redux’s useSelector re-renders only when the slice you selected changes. That is the biggest practical difference.
  • No middleware. Handling async, logging and persistence in one place is all yours to build.
  • No DevTools. No time travel, no action log, no state diff.
ContextRedux
Good forTheme, current user, locale — rarely changesChanges often, read and written in many places, needs a debuggable trail
Subscription granularityThe whole valuePer selector
Extra dependencyNoneYes
BoilerplateLittleSome (far less since Redux Toolkit)

Follow-up: “Do people still use Redux?” — answer honestly: plenty of projects moved pure client state to Zustand or Jotai (lighter); server data goes to TanStack Query or SWR (caching, deduping and retries are their day job, and Redux doing it is a stretch). Redux’s remaining strength is complex client state with a lot of interaction logic that you need to be able to trace, and you should always use Redux Toolkit rather than write it by hand.

React 与生态React & ecosystem#350

Redux 的结构和工作流

Redux structure and workflow

看答案Show answer

一句话:单向环—— view 派发 action → 中间件处理 → reducer 算出新 state → store 更新 → 订阅的组件重渲染。

五个角色:

  • store——唯一的状态容器, 提供 getState /dispatch /subscribe
  • action——描述「发生了什么」的普通对象,必须有 type它只描述,不做事。
  • reducer——(state, action) => newState必须是纯函数
  • middleware—— 在 action 到 reducer 之前拦一道(#354)。
  • selector—— 从 store 里挑出组件需要的那部分。

Redux Toolkit(RTK)改变了什么—— 必答,因为现在没人手写 Redux 了:

  • createSlice一次生成 reducer + action creators + action types, 样板代码少一大半。
  • 内置 Immer, 所以你可以写起来像在改state.list.push(x), 实际产出的是新对象—— 但要注意这只在createSlice 里成立。
  • 默认装好 thunk 和 DevTools。
  • createAsyncThunk管异步的三个状态 (pending / fulfilled / rejected)。

会追问:「为什么必须单向?」—— 因为状态变化的路径唯一, 所以出 bug 时可以从 action 记录里 倒推每一步。 双向绑定的框架里 「这个值到底是谁改的」经常查不清。

In one line: a one-way loop — the view dispatches an action, middleware handles it, a reducer computes the new state, the store updates, and the subscribed components re-render.

Five roles:

  • store — the single container for state, exposing getState, dispatch and subscribe.
  • action — a plain object that describes what happened and must have a type. It only describes; it does nothing.
  • reducer (state, action) => newState, and it has to be pure.
  • middleware — intercepts the action on its way to the reducer (#354).
  • selector — picks the part of the store a component needs.

What Redux Toolkit (RTK) changed — you have to cover this, because nobody hand-writes Redux any more:

  • createSlice generates the reducer, the action creators and the action types at once, cutting more than half the boilerplate.
  • Immer is built in, so you can write what looks like a mutation — state.list.push(x) — and still get a new object out. Just remember this only holds inside createSlice.
  • thunk and DevTools are wired up by default.
  • createAsyncThunk handles the three async states (pending / fulfilled / rejected).

Follow-up: “Why does it have to be one-way?” — because there is exactly one path a change can take, so when something breaks you can walk back through the action log step by step. In a two-way binding framework, “who actually changed this value” is often unanswerable.

JavaScript现在真正会写的 Redux示意Illustrative
1// RTK 的 slice:reducer + actions 一次生成
2const todos = createSlice({
3 name: "todos",
4 initialState: [],
5 reducers: {
6 add(state, action) {
7 state.push(action.payload); // 看着是 mutate,Immer 会产出新 state
8 },
9 toggle(state, action) {
10 const t = state.find((x) => x.id === action.payload);
11 if (t) t.done = !t.done;
12 },
13 },
14});
15
16export const { add, toggle } = todos.actions;
17
18// 组件里
19const list = useSelector((s) => s.todos); // 只订阅这一部分
20const dispatch = useDispatch();
21dispatch(add({ id: Date.now(), text, done: false }));
1// An RTK slice generates the reducer and the actions together
2const todos = createSlice({
3 name: "todos",
4 initialState: [],
5 reducers: {
6 add(state, action) {
7 state.push(action.payload); // it looks like a mutation; Immer produces a new state
8 },
9 toggle(state, action) {
10 const t = state.find((x) => x.id === action.payload);
11 if (t) t.done = !t.done;
12 },
13 },
14});
15
16export const { add, toggle } = todos.actions;
17
18// In the component
19const list = useSelector((s) => s.todos); // subscribes to this part only
20const dispatch = useDispatch();
21dispatch(add({ id: Date.now(), text, done: false }));
React 与生态React & ecosystem#352

Redux 的三大原则

Redux 3 main principles

看答案Show answer

三条,每条都要说出「为什么」:

  1. 单一数据源(single source of truth)—— 整个应用一个 store。
    为什么:状态只有一份就不会不一致, 而且整个应用的状态可以被序列化—— 这才有了「保存/恢复现场」和 SSR 脱水注水。
  2. state 只读—— 只能通过派发 action 改。
    为什么:把「谁能改状态」收窄到一个入口, 于是所有变化都可以被记录、 可以被拦截、可以被回放。
  3. 用纯函数(reducer)做修改——(state, action) => newState
    为什么:纯函数给定同样的 state 和 action 永远得到同样结果, 所以能重放、能测试、时间旅行调试才成立

这三条是一个整体: 单一数据源让状态可序列化, 只读让变化可记录, 纯 reducer 让变化可重放 ——三者合起来才有 DevTools 的时间旅行。能把它们串起来讲比一条条背强得多。

会追问:「reducer 里能做什么不能做什么?」——不能:改传进来的 state、 发请求、读Date.now() /Math.random()、 派发别的 action。这些都放中间件或 action creator 里。
「RTK 里 state.push()违反第 2 条吗?」—— 不违反。Immer 给的是一个草稿代理(draft proxy), 你的修改被记录下来, 最终产出的是新对象,原 state 没动。

Three of them, and each one needs a “why”:

  1. Single source of truth — one store for the whole app.
    Why: one copy of the state cannot disagree with itself, and the entire app state can be serialised — which is what makes “save and restore the session” and SSR dehydration possible.
  2. State is read-only — you change it only by dispatching an action.
    Why: it narrows “who can change state” down to one entrance, so every change can be logged, intercepted and replayed.
  3. Changes are made by pure functions (reducers) (state, action) => newState.
    Why: a pure function always gives the same result for the same state and action, so it can be replayed and tested, and that is what makes time-travel debugging work.

The three are one package: a single source of truth makes state serialisable, read-only makes changes loggable, and pure reducers make changes replayable — together they add up to time travel in DevTools. Tying them together like this is much stronger than reciting them one by one.

Follow-up: “What can and cannot a reducer do?” — it cannot mutate the state it was given, make requests, read Date.now() or Math.random(), or dispatch other actions. All of that belongs in middleware or an action creator.
“Does state.push() in RTK break rule 2?” — no. Immer hands you a draft proxy, records your edits, and produces a new object; the original state is untouched.

这些题从哪来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.