React 组件根据 props 和当前 state 返回界面描述。渲染阶段应保持纯净,不能在函数体里发请求、写存储或启动订阅。React 决定需要提交的变化后再更新宿主界面;并非每次组件函数执行都对应一次可见 DOM 更新。

状态属于一次渲染

import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => {
    setCount(value => value + 1);
    setCount(value => value + 1);
  }}>计数 {count}</button>;
}

函数式更新根据队列里的前值计算下一值,因此一次点击增加 2。若两行都写 setCount(count + 1),它们读取同一轮渲染里的 count,不会依次读取被原地修改的变量。定时器中的闭包同样可能保留旧渲染的值,要根据业务选择函数式更新、依赖管理或 ref。

props 由父组件传入,修改请求通过回调传回。对象和数组更新应创建新值;直接 push 后传回原数组,可能让更新与记忆化判断失去可靠依据。派生值优先在渲染中计算,不要写 Effect 把过滤后的列表再复制成另一份 state。

身份和表单

同一位置、类型与 key 共同影响组件身份。列表用稳定的业务 ID 作为 key;索引在插入和排序后可能把某项输入状态保留给另一条记录。key 不是普通 props,也不应在每次渲染时随机生成。

受控输入用 value 和 onChange 保持一致;checkbox 使用 checked。草稿可以留在表单组件,提交后才进入业务列表。ref 可保存 DOM 引用或不参与显示的可变值,改变 ref 不会自动触发渲染。

Effect 用来同步外部系统

import { useEffect, useState } from "react";

export function OnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine);
  useEffect(() => {
    const update = () => setOnline(navigator.onLine);
    window.addEventListener("online", update);
    window.addEventListener("offline", update);
    return () => {
      window.removeEventListener("online", update);
      window.removeEventListener("offline", update);
    };
  }, []);
  return <p>{online ? "浏览器报告在线" : "浏览器报告离线"}</p>;
}

这是浏览器专用组件;navigator.onLine 也不保证目标服务可访问。Effect 的清理需要撤销对应订阅。开发中的 StrictMode 可能额外执行设置与清理以暴露问题,不能靠去掉 StrictMode 掩盖重复连接。Hook 放在组件或自定义 Hook 顶层,不放条件和普通循环里。

请求也属于外部同步,但查询缓存、路由数据层可能比手写一组 Effect 更合适。若自己请求,处理取消、旧响应和错误;不要为了避开依赖提示随意清空依赖数组。SSR 环境不能在渲染时直接读取 window 或 navigator。

练习:把 Counter 改成可调整步长,比较直接更新和函数式更新;给 OnlineStatus 反复挂载卸载,检查监听器是否释放。继续完成学习记录板。

面试自查:连续两次直接更新为什么可能只增加一次?改变 key 会怎样影响本地状态?Effect 的空依赖、无依赖数组与包含依赖分别意味着什么?

参考:状态快照、列表 key、Effect 同步。