DrillLab
第 12 / 25 节LESSON 12 / 25约 22 分钟~22 min

Redux 与 TypeScript · 六问6 questions on Redux and TypeScript

Redux vs Context、结构与工作流、三大原则、中间件、JS vs TS、静态类型检查。Redux vs Context, the parts and the data flow, the three principles, middleware, JS vs TS, static type checking.

面试 · 第 5 部分Interview · Part 5
这一页有什么On this page7
学完这节你会After this lesson you can
  • 说清 Redux 和 Context 解决的不是同一个问题Explain that Redux and Context do not solve the same problem
  • 画出 action → middleware → reducer → store → view 的完整流转Draw the full path: action to middleware to reducer to store to view
  • 背出三大原则并解释每一条为什么必要State the three principles, and explain why each one is needed
  • 说明静态类型检查在什么阶段发现什么问题Say at which stage static type checking finds a problem, and which kind of problem
这在考试里考什么What the exam does with this

只要简历上写了 Redux,这四道基本会连着问。「Redux vs Context」是最容易答错的一道 —— 说「Context 能替代 Redux」或者反过来都不对。TS 那两道是现在的标配题。If Redux is on your resume, these four questions usually come one after another. "Redux vs Context" is the one people get wrong most often: saying Context can replace Redux is wrong, and so is the opposite. The two TypeScript questions are now standard.

§01

Redux vs Context API

#349 Redux vs Context API

一句话(这句最关键):它们解决的不是同一个问题。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.

§02

Redux 的结构和工作流What are the parts of Redux and how does data flow through them?

#350 Redux structure and workflow

一句话:单向环—— 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 }));
§03

Redux 的三大原则What are the three principles of Redux?

#352 Redux 3 main principles

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

  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.

§04

解释一下 Redux 中间件Explain Redux middleware

#354 explain Redux Middleware

一句话:中间件是夹在「派发 action」和 「reducer 收到 action」之间的一层, 能拦截、改写、延迟、 甚至吞掉一个 action。

签名是三层柯里化—— 这个形状本身常被问到:store => next => action => {}。 多个中间件靠 next串成一条链,和 Express 的中间件是同一个模式

为什么需要它:因为reducer 必须是纯的, 所以异步和副作用无处安放。 中间件就是专门给副作用留的位置

常见的几个:

  • redux-thunk—— 让你能 dispatch 一个函数而不只是对象, 在里面做异步。最简单,RTK 默认装。
  • redux-saga—— 用 generator 描述复杂异步流程 (可取消、可重试、能编排多个请求)。 能力强但学习成本高。
  • redux-logger—— 打印每个 action 前后的 state。

会追问:「thunk 和 saga 怎么选?」—— 大部分项目 thunk 够了;只有在需要「取消、去抖、 复杂的流程编排」时 saga 才值那份复杂度
「能自己写一个吗?」—— 能,而且面试常让手写一个 logger。

In one line: middleware is a layer between “an action is dispatched” and “the reducer receives it”. It can intercept, rewrite, delay or even swallow an action.

The signature is curried three levels deep — the shape itself gets asked about: store => next => action => {}. Several middlewares chain together through next, and it is the same pattern as Express middleware.

Why you need it: because the reducer has to be pure, async work and side effects have nowhere to live. Middleware is the place reserved for side effects.

The common ones:

  • redux-thunk — lets you dispatch a function instead of only an object, and do your async work inside it. Simplest option, and RTK installs it by default.
  • redux-saga — describes complex async flows with generators (cancellable, retryable, able to orchestrate several requests). Powerful, but a steep learning curve.
  • redux-logger — prints the state before and after each action.

Follow-up: “thunk or saga?” — thunk is enough for most projects; saga only earns its complexity when you need cancellation, debouncing or real flow orchestration.
“Could you write one?” — yes, and interviewers often ask you to write a logger on the spot.

JavaScript中间件的形状与 thunk示意Illustrative
1// 手写一个 logger 中间件:注意那三层箭头
2const logger = (store) => (next) => (action) => {
3 console.log("派发:", action.type, action.payload);
4 const result = next(action); // 交给下一个中间件 / reducer
5 console.log("新状态:", store.getState());
6 return result;
7};
8
9// thunk 让 dispatch 能收函数
10const fetchUser = (id) => async (dispatch) => {
11 dispatch({ type: "user/loading" });
12 try {
13 const res = await fetch(`/api/users/${id}`);
14 if (!res.ok) throw new Error(`HTTP ${res.status}`); // 别忘了这一句
15 dispatch({ type: "user/loaded", payload: await res.json() });
16 } catch (e) {
17 dispatch({ type: "user/failed", payload: e.message });
18 }
19};
1// Writing a logger middleware yourself: note the three levels of arrows
2const logger = (store) => (next) => (action) => {
3 console.log("dispatching:", action.type, action.payload);
4 const result = next(action); // hand it to the next middleware or the reducer
5 console.log("new state:", store.getState());
6 return result;
7};
8
9// thunk lets dispatch accept a function
10const fetchUser = (id) => async (dispatch) => {
11 dispatch({ type: "user/loading" });
12 try {
13 const res = await fetch(`/api/users/${id}`);
14 if (!res.ok) throw new Error(`HTTP ${res.status}`); // do not forget this line
15 dispatch({ type: "user/loaded", payload: await res.json() });
16 } catch (e) {
17 dispatch({ type: "user/failed", payload: e.message });
18 }
19};
§05

JavaScript vs TypeScript

#355 Javascript vs TypeScript

一句话:TS 是 JS 的超集—— 加了静态类型,编译后就是普通 JS, 运行时没有任何 TS 的东西

JavaScriptTypeScript
类型检查运行时才炸编译期就报
需要构建不需要需要(tsc / esbuild / SWC)
IDE 支持靠猜精确补全、跳转、重命名
重构靠搜字符串改一处,所有不兼容的地方都报出来

「运行时没有 TS」这句要强调, 因为它推出两个重要结论:

  • 类型不能用来做运行时校验。接口返回的数据是不是真的符合你写的interface, TS 管不了—— 要校验得用 zod 这类库。这是新手最大的误解。
  • as 断言只是「我保证」, 不做任何检查。滥用 asany 等于关掉了 TS。

代价(要主动说):多一步构建、 有学习成本(泛型、 条件类型、unknown vs any)、 第三方库缺类型时要自己写声明、 复杂类型报错很难读。

会追问:interfacetype 选哪个?」——interface能被重复声明合并、 更适合描述对象和 class 契约;type 能写联合、 交叉、映射、条件类型,能力更全。 实践上「对象形状用 interface, 其他用 type」,但团队统一比选哪个更重要

In one line: TS is a superset of JS — it adds static types, and after compilation it is ordinary JS with nothing of TS left at runtime.

JavaScriptTypeScript
Type checkingFails at runtimeReported at compile time
Build stepNot neededNeeded (tsc / esbuild / SWC)
IDE supportGuessworkPrecise completion, go-to-definition, rename
RefactoringSearch for stringsChange one place and every incompatible use lights up

Stress the “nothing of TS at runtime” line, because two important conclusions follow from it:

  • Types cannot validate anything at runtime. Whether the data an API returns really matches the interface you wrote is beyond TS — for that you need something like zod. This is the biggest beginner misconception.
  • An as assertion is just “trust me” and checks nothing. Overusing as and any is the same as turning TS off.

The costs — bring them up yourself: an extra build step, a learning curve (generics, conditional types, unknown vs any), writing your own declarations when a library ships none, and error messages for complex types that are hard to read.

Follow-up:interface or type?” — interface can be declared again and merged and suits object and class contracts; type can do unions, intersections, mapped and conditional types, so it is more capable. In practice: “interface for object shapes, type for everything else” — but a consistent team choice matters more than which one you pick.

§06

什么是静态类型检查,有什么好处What is static type checking, and what does it give you?

#356 What is static type checking and how can developers benefit from it

一句话:不运行代码,只靠分析源码就找出类型不匹配的地方。 「静态」的意思就是「在编译期,而非运行期」。

四个具体收益(要给例子,别空谈):

  • 错误提前——user.nmae 拼错、 忘了处理 null、 给函数传少了参数,在编辑器里就红了, 而不是上线后用户报给你
  • 类型即文档—— 函数签名说明了它要什么、给什么。而且这份文档不会过期, 因为改了代码不改类型就编译不过。
  • 重构有底气—— 改一个字段名, 所有受影响的地方都会报错。这是 TS 最被低估的价值, 在大项目里比「防 bug」更实用。
  • IDE 能力—— 精确补全、跳定义、 安全重命名。

局限(说出来才显得懂):它只保证「类型对」,不保证「逻辑对」—— 类型全过的代码照样能算错工资。 而且它管不到运行时的外部数据(见 #355),所以类型检查不能替代测试

会追问:strict 模式开不开?」——新项目一定开。 最有价值的是strictNullChecks—— 它把「忘了判空」这一整类 运行时错误变成编译错误。
顺带一个真实例子:React 那门课的源项目npm run build 就是因为tsc 报了 10 个错误而失败的 (测试文件缺 vitest 全局类型)—— 这说明类型检查是构建的一部分, 不是可选的 lint

In one line: without running the code, purely by analysing the source, it finds places where the types do not line up. “Static” just means “at compile time, not at run time”.

Four concrete benefits — give examples, do not speak in the abstract:

  • Errors surface earlier — a typo like user.nmae, a forgotten null case, a missing argument: they go red in the editor instead of arriving as a user report after release.
  • Types are documentation — a signature says what it wants and what it gives back. And this documentation cannot go stale, because changing the code without changing the types fails the build.
  • Refactoring with confidence — rename one field and every affected place errors. This is TS’s most underrated value, and on a large codebase it is more useful than bug prevention.
  • Editor power — accurate completion, jump to definition, safe rename.

The limits — saying them is what shows you get it: it only guarantees the types are right, not that the logic is — fully typed code can still calculate the wrong salary. And it has no reach over external data at runtime (see #355), so type checking is not a substitute for tests.

Follow-up: “Do you turn on strict?” — always on a new project. The most valuable piece is strictNullChecks — it turns an entire class of “forgot the null check” runtime errors into compile errors.
A real example to go with it: npm run build on the source project for the React course fails precisely because tsc reports 10 errors (the test file is missing the vitest globals) — which shows type checking is part of the build, not an optional lint.

迁移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.

问 Redux vs ContextAsked about Redux vs Context
一个是传递方案一个是状态管理;Context 缺精细订阅、中间件、DevToolsOne is a way to pass data down, the other is state management; Context has no per-field subscriptions, no middleware and no DevTools
「context 一变全都重渲染」One Context change re-renders everything
拆 Context,或换 selector 型状态库Split the Context into smaller ones, or move to a state library where each component selects the fields it reads
reducer 里想发请求You want to send a request from inside a reducer
挪到中间件或 thunk,reducer 必须纯Move it into middleware or a thunk; a reducer has to be pure
问三大原则Asked for the three principles
串起来讲:可序列化 → 可记录 → 可重放 = 时间旅行Tell them as one chain: state can be serialised, so changes can be recorded, so they can be replayed, which is time travel
问服务端数据怎么管Asked how to manage data from the server
TanStack Query / SWR,别用 Redux 硬凑缓存TanStack Query or SWR; do not build a cache out of Redux
以为 TS 类型能校验接口数据Expecting a TypeScript type to validate data from an API
运行时没有 TS,要用 zodNo TypeScript is left at runtime, so validate with something like zod
这节的要点What to take away
  1. Context 管传递,Redux 管状态管理;Context 缺精细订阅、中间件、DevTools 三样。Context passes data down, Redux manages state. Context is missing three things: per-field subscriptions, middleware, and DevTools.
  2. Redux 单向环:dispatch → middleware → reducer → store → view;现在一律用 RTK 的 createSlice。The one-way loop in Redux: dispatch, then middleware, then reducer, then store, then view. Today always write it with createSlice from RTK.
  3. 三大原则串起来才是重点:单一数据源→可序列化,只读→可记录,纯 reducer→可重放。The three principles matter as a chain: one source of truth means the state can be serialised, read-only state means changes can be recorded, pure reducers mean they can be replayed.
  4. 中间件签名 store => next => action,是专门给副作用留的位置;thunk 够用,saga 只在需要编排时值。Middleware has the signature store => next => action, and it is the place set aside for side effects. thunk is enough for most cases; saga is only worth it when you have to coordinate several steps.
  5. TS 编译后运行时什么都不剩 —— 所以类型不能校验外部数据,as 只是「我保证」。Nothing of TypeScript is left after compiling, so a type cannot validate data from outside, and as only means "trust me".
  6. 静态检查最被低估的价值是重构有底气;但它只保证类型对不保证逻辑对,不能替代测试。The most underrated value of static checking is the confidence to refactor. But it only guarantees the types are right, not the logic, so it does not replace tests.

接下来What next

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