React 面试中如何设计 state 结构:状态放哪、如何分组、如何重置?
【免费下载链接】front-end-interview-handbookFront End interview preparation materials for busy engineers (updated for 2026)项目地址: https://gitcode.com/GitHub_Trending/fr/front-end-interview-handbook
在 React 的 UI 编码面试题中,state 设计往往决定了解答的整体质量。front-end-interview-handbook 项目的 React 面试手册 react-state-design 章节(中文版)给出了明确判断:Good state design is often what separates strong interview solutions from weak ones。本文按该文档的内容把 state 设计整理成一条可以照着执行的流程:先枚举 UI 真正需要的最小状态集合,再决定状态放在哪一层、如何分组、以及如何重置,最后说明面试场景下 context 和useReducer通常可以不要的理由。
动笔之前:先列出 UI 需要的最小状态集合
文档把这一步放在最前面,原因是面试官经常追问 state 结构本身,而干净的 state 形状一旦确定,后面的实现会自然展开:
Before you start coding, spend a minute identifying the minimal set of values the UI actually needs.
也就是说,在写下第一个useState之前,先只列出 UI 渲染和交互真正依赖的值。列出之后,下面三节的原则都作用在这份清单上:哪些该留在局部、哪些该合并、哪些根本不该存。
状态放哪:能局部就局部,面试题默认放顶层
只被一个组件使用的状态不要上提
文档给出的反例是把只有Child使用的count提升到App,再通过 props 把count和setCount传下去。正确做法是让状态留在使用它的组件内部:
function App() { return <Child />; } function Child() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>Count: {count}</button>; }区别在于重渲染范围:count变化时只有Child需要重渲染,而不是父组件带动整棵子树。
面试场景的默认选择:状态放顶层,子组件保持无状态
文档在 "What you need to know for interviews" 中给出了面试专用判断:由于面试题体量小,state 大概率应该放在顶层 / app 层,大多数子组件应该是 stateless 的、通过 props 接收数据。只有两种例外适合把状态留在子组件里:
- 表单输入框里的临时状态(ephemeral state in form inputs);
- 不需要跨整个应用共享的状态。
状态如何分组:三条判断准则
把表示同一件事的字段合并进一个 state 对象
x和y共同定义一个坐标点,就该作为一个单元管理。分开两个useState时,每次移动指针都要连续调用setX(e.clientX)和setY(e.clientY)两个 setter,更新麻烦且容易漏掉其中一个。合并后一次更新完成:
function App() { const [point, setPoint] = useState({ x: 0, y: 0 }); return ( <div onPointerMove={(e) => { setPoint({ x: e.clientX, y: e.clientY }); }}> <p> Point: ({point.x}, {point.y}) </p> </div> ); }用单一 status 字段消灭互相矛盾的状态
文档用一个提交表单的例子说明问题:如果用isSubmitting、isSubmitted、isError三个布尔值表示表单状态,就可能出现setIsSubmitting(true)和setIsSubmitted(true)同时为真的组合——组件"正在提交"且"已提交成功",这在语义上不成立。修复方式是把互斥状态合并成一个字符串枚举,任何时刻只可能处于其中一个值:
function ContactForm() { const [status, setStatus] = useState('idle'); // "idle", "submitting", "success", "error" function handleSubmit(event) { event.preventDefault(); setStatus('submitting'); fetch('/api/submit', { method: 'POST' }) .then(() => setStatus('success')) .catch(() => setStatus('error')); } return ( <form onSubmit={handleSubmit}> <button type="submit" disabled={status === 'submitting'}> Submit </button> {status === 'success' && <p>Form submitted successfully!</p>} {status === 'error' && <p>Submission failed. Please try again.</p>} </form> ); }文档给出的好处:表单任一时刻只能处于一个状态、错误路径不会把表单错误标记成已提交、也不再需要手动同步多个布尔值。
能算出来的值就在渲染时算,不要存
如果同时存了todos列表和一个count,还要用useEffect在todos变化时把count同步成todos.length,那就制造了两个必须保持一致的真相来源。正确写法是渲染期间直接派生:
const [todos, setTodos] = useState(['Task 1', 'Task 2']); const count = todos.length; // No need to store it separately文档的说明是:每次渲染重新计算一个普通变量的开销很小,却能消除一整类"状态过期"的 bug。判断标准一句话:凡是能从现有 state 或 props 计算出来的值,就在渲染时派生。
状态如何重置
最直接的重置方式是把每个 state 设回初始值,但状态字段一多就要逐个调用 setter,容易漏字段。文档给出两条路:
- 字段级重置:把状态分组,并为每种可能的操作定义函数,重置时调用这些函数而不是散落的 setter;
- 整体重置(文档推荐的"最干净"方式):改变组件的
key。key变化时 React 会卸载并重建该元素,子树里所有useState、ref 和 effect 拥有的资源全部清空:
function Form() { return ( <form> <input type="text" placeholder="John Doe" /> <button>Submit</button> </form> ); } function App() { const [key, setKey] = useState(0); return ( <div> <Form key={key} /> <button onClick={() => setKey((prev) => prev + 1)}>Reset form</button> </div> ); }工作机制是key变化强制 React 对Form执行 unmount + remount。文档同时指出:<form>本身有formElement.reset()这类内置重置方式,但key方案对任意组件都有效,尤其适合内部状态多、逐字段重置很麻烦的场景。
可选的进一步封装:把重置封装进自定义 hook,调用方只能通过暴露的操作(如increment、decrement、reset)修改状态,拿不到裸的 setter。react-hooks 章节 中的useCounter示例就是这个模式:
function useCounter(initialValue = 0) { const [count, setCount] = useState(initialValue); const increment = () => setCount(count + 1); const decrement = () => setCount(count - 1); const reset = () => setCount(initialValue); return { count, increment, decrement, reset }; }面试中还用不用 Context 和 useReducer
这两问决定了"状态放哪"的边界,文档给出的面试判断是:
- 大概率不需要 context:面试题的 prop 传递深度在最坏情况下也不超过两三层,直接 prop passing 更简单。
- 大概率不需要 reducer:面试题的状态变更种类有限,把状态更新收敛到几个 action 函数里即可;例外是游戏类题目,游戏逻辑可以复杂到值得上
useReducer。
如果确实使用了 context,注意文档列出的陷阱中最影响面试代码的两条:provider value 每次变化都会让所有消费该 context 的组件重渲染,所以频繁更新的数据(输入框内容、鼠标位置)不要放进 provider,应留在局部useState;不要把太多不相关的数据塞进同一个 provider,按更新频率或关注点拆分成多个 provider。
自检与练习入口
写代码前可以按文档的原则过一遍清单,全部满足再动笔:
- 是否已列出 UI 需要的最小状态集合,可派生的值(如
todos.length)已改为渲染时计算; - 相关字段(如
x/y)是否合并进了同一个 state 对象; - 互斥状态(如提交中/成功/失败)是否合并成单个 status 字段,不存在矛盾组合;
- 除表单临时状态和局部状态外,状态是否都在顶层、子组件是否 stateless;
- 需要整体重置时,是否优先考虑改
key而不是逐个字段还原。
react-state-design 文档末尾的 Practice questions 一栏给出了配套的练习入口,包括 Todo List、Transfer List、Users Database、Wordle、Undoable Counter 等 coding 题,以及 controlled/uncontrolled 组件、context 陷阱、"如何重置组件 state"等 quiz 题,可以用来验证上述原则是否真正可用。完整的状态设计原则、context 适用场景(主题、鉴权、路由、通知、全局弹窗等)和 reducer 适用场景(多步表单、状态驱动 UI、有限状态机)见 react-state-design 原文,useState的陷阱与函数式更新写法见 react-hooks。
【免费下载链接】front-end-interview-handbookFront End interview preparation materials for busy engineers (updated for 2026)项目地址: https://gitcode.com/GitHub_Trending/fr/front-end-interview-handbook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考