Redux Toolkit 版 TodoTodo with Redux Toolkit
题面The problem
先把要求读完,再动手。Read every requirement before you start.
四个空。第 3 个空是这道题的加分点, 第 4 个空写错会让 reducer 不纯。
Four blanks. The 3rd one is the bonus point of this question; get the 4th one wrong and the reducer is no longer pure.
- 用 createSlice 写出 added / toggled / removed / clearedDone / filterChangedUse createSlice to write added, toggled, removed, clearedDone and filterChanged
- id 在 prepare 里生成,不在 reducer 里 —— reducer 必须纯,同一个 action 跑两次结果要一样The id is generated in prepare, not in the reducer. A reducer has to be pure: running the same action twice must give the same result
- Immer 给的是草稿代理,但传进来的 state 对象本身不能被改动(测试用冻结的 state 验)Immer hands you a draft proxy, but the state object passed in must not be changed. The test proves it with a frozen state
- 派生数据用 selector:selectVisible / selectRemaining / selectFilterDerived data goes through selectors: selectVisible, selectRemaining and selectFilter
- 筛选不能改底层数据 —— 切回 all 时全部条目都还在Filtering must not change the underlying data. Switching back to all brings every item back
- reducer 要能脱离 React 单独测(所以这道题的测试里一行 render 都没有)The reducer must be testable on its own, without React. That is why the tests for this problem never call render
预计 40 分钟。这个数字是照「读完题就开始写、不查资料」估的 —— 第一次超时很正常,第二次要压进去。Budget 40 minutes. Overrunning on the first pass is normal; the second pass should fit.
工作区Workspace
工作区是一个真的浏览器沙箱:左边写代码,右边实时预览,下面一个「跑测试」按钮。测试和本机那套是同一批断言,转写成了浏览器里能跑的写法。The workspace is a real in-browser sandbox: edit on the left, live preview on the right, one Run button below. The assertions are the same ones that pass on a real machine, rewritten for the browser runner.
需要联网。Requires an internet connection. 打包器和 npm 依赖都在 CodeSandbox 的远程服务上(评估过程见 docs/sandpack-evaluation.md),断网这块就起不来 —— 那就照下面的命令在本机跑。The bundler and the npm packages come from CodeSandbox's remote service, so this panel needs network access.
展开讲解Walkthrough
下面是《缺口三 · 同一个 Todo 换成 Redux Toolkit》那一节的原文 —— 和课程里是同一份内容,不是另写的摘要。卡住了再展开。Below is the actual text of the lesson “缺口三 · 同一个 Todo 换成 Redux Toolkit” — the same content as in the course, not a rewritten summary. Expand it when you stall.
展开《缺口三 · 同一个 Todo 换成 Redux Toolkit》(3 段 · 约 24 分钟)Expand “缺口三 · 同一个 Todo 换成 Redux Toolkit” (3 sections · ~24 min)
完整那一节(含练习、常见错误、迁移模式)在The full lesson (with exercises, common mistakes and transfer patterns) is at 缺口三 · 同一个 Todo 换成 Redux Toolkit。
createSlice:一次生成 reducer 和 actionscreateSlice: one call gives you the reducer and the actions
手写 Redux 的那套样板已经过时了。The hand-written Redux boilerplate is out of date.
老写法要写三份: action types 常量、 action creators、 一个大 switch 的 reducer。createSlice把三份合成一份—— 你只写 reducers 对象, 它自动生成对应的 action creator 和 type (type 是 "todos/added"这样的 切片名/reducer 名)。
Immer 那一点必须说清:state.items.push(...)看起来违反了「state 只读」, 其实你拿到的是一个草稿代理(draft proxy)—— 你的修改被记录下来, Immer 最终产出一个新对象, 原 state 一个字节没动。测试里我用expect(next).not.toBe(empty)和「原 state 长度仍是 0」两条断言证明了这一点。
但有个边界要记住: 这个「能 mutate」的特权只在 createSlice /createReducer 里成立。 在组件里、在 selector 里、 在自己写的普通函数里,该守的不可变规则一条都不能少。
prepare 是这道题的加分点。生成 id 要用nanoid(), 而 reducer 必须是纯函数 —— 不能出现随机数和时间(否则同样的 state + action 会得到不同结果,时间旅行就废了)。prepare让你在创建 action 的时候生成 id,reducer 只负责把它放进去。
The old way needed three pieces: action type constants, action creators, and a reducer built around one big switch. createSlice folds the three into one — you only write the reducers object and it generates the matching action creator and type (the type is "todos/added", that is, sliceName/reducerName).
The Immer part has to be said clearly: state.items.push(...) looks like it breaks “state is read-only”, but what you hold is a draft proxy — your edits get recorded and Immer produces a new object at the end, leaving the original state untouched byte for byte. The test proves it with two assertions: expect(next).not.toBe(empty) and “the original state still has length 0”.
But there is a boundary to remember: this mutate-anyway privilege only holds inside createSlice / createReducer. In components, in selectors, in your own plain functions, every immutability rule still applies.
prepare is the bonus point of this question. Generating the id needs nanoid(), and a reducer has to be pure — no random numbers, no clock (otherwise the same state + action gives different results and time travel is dead). prepare lets you generate the id while the action is being created, so the reducer only puts it in place.
selector:这才是 Redux 比 Context 强的地方Selectors: this is where Redux beats Context
三个 useSelector 各自订阅一小块。Three useSelector calls, each subscribing to one small part.
组件里写了三个 useSelector: 可见列表、剩余条数、当前筛选。每一个只在自己那部分变化时触发重渲染。
对比 Context(变式五那道题): context value 一变,所有 useTheme()的组件全部重渲染, 哪怕它只用了 theme而变的是 toggleTheme。这就是八股 #349 里说的「Context 缺精细订阅」, 在这里能具体看到。
一个必须知道的坑: selector 不要返回新对象。useSelector默认用 === 比较结果, 返回 { a, b }这样的新对象每次都不相等, 于是每次 store 有任何变化都重渲染 —— 等于优化白做。
解法:拆成多个useSelector(本实现的做法,最简单), 或者用 createSelector做记忆化,或者传shallowEqual 当第二个参数。
注意 selectVisible是从 items 派生的, 筛选不改底层数据—— 和变式一里「写操作必须作用于完整数据」 是同一条规矩。测试里专门有一条验证「筛完底层仍是两条」。
The component has three useSelector calls: the visible list, the remaining count, the current filter. Each one re-renders only when its own slice changes.
Compare with Context (the variant 5 question): the moment the context value changes, every useTheme() component re-renders, even one that only uses theme while what changed was toggleTheme. That is the “Context has no fine-grained subscription” point from interview #349, in concrete form.
One trap you have to know: a selector must not return a new object. useSelector compares its result with === by default, and a fresh object like { a, b } is never equal, so any change anywhere in the store re-renders — the optimization cancels itself out.
Fixes: split it into several useSelector calls (what this implementation does, and the simplest), memoize with createSelector, or pass shallowEqual as the second argument.
Note that selectVisible is derived from items, and filtering does not touch the underlying data — the same rule as “writes have to act on the complete data” in variant 1. One test is there to verify that after filtering the underlying data still has two entries.
和变式一(useState 版)对比:换来了什么,代价是什么Compared with variant one, the useState version: what you gain and what it costs
| 变式一(useState) | 本题(Redux Toolkit) | |
|---|---|---|
| 状态放哪 | 组件内 | 全局 store |
| 谁能改 | 这个组件 | 任何组件 dispatch 就行 |
| 逻辑能不能脱离 React 测 | 不能(要 render) | 能—— reducer 是纯函数 |
| 调试 | console.log | DevTools 能看每个 action 和 state diff |
| 代码量 | 一个文件 | slice + store + 组件,三个文件 |
| 依赖 | 无 | 两个包 |
结论怎么说(面试里问「该不该上 Redux」时用):这道题的规模用 Redux 是过度设计—— 一个组件自己的列表,useState 就够。Redux 的价值要在 「同一份状态被很多不相关的组件读写」 或者「需要按 action 追溯 bug」 的时候才兑现。
能主动说出「这题我会用 useState, 但如果需求是 XX 我会上 Redux」, 比闷头写完 Redux 更能显示判断力。
顺带看得出的一个真实收益: 因为 reducer 是纯函数,八条测试里有五条完全不需要渲染任何组件—— 直接 reducer(state, action) 断言。 测试跑得更快、失败信息更准。
| Variant 1 (useState) | This one (Redux Toolkit) | |
|---|---|---|
| Where the state lives | inside the component | a global store |
| Who can change it | this component | any component that dispatches |
| Can the logic be tested without React | no (you have to render) | yes — the reducer is a pure function |
| Debugging | console.log | DevTools shows every action and the state diff |
| Amount of code | one file | slice + store + component, three files |
| Dependencies | none | two packages |
How to phrase the conclusion (for when they ask “should we bring in Redux?”): at this size Redux is over-engineering — one component’s own list is fine with useState. Redux pays off when the same state is read and written by many unrelated components, or when you need to trace a bug action by action.
Saying “I would use useState here, but if the requirement were XX I would reach for Redux” shows more judgment than silently writing the whole Redux setup.
One real benefit that falls out of this: because the reducer is a pure function, five of the eight tests render no component at all — they assert on reducer(state, action) directly. The tests run faster and the failure messages are more precise.
参考答案Reference solution
这道题没有配套的分级提示 —— 卡住了先看上面的讲解那一节。This problem has no graded hints. If you are stuck, read the walkthrough above first.
这份答案在本机真跑过测试。但先确认你自己动手写过一遍 —— 读懂答案和写出答案是两种能力,考场上考的是后一种。This answer really was run here and its tests passed. But write it yourself first — reading an answer and producing one are two different skills, and the exam tests the second.