useState 保存函数时,函数为什么被提前调用
裸函数会被视为initializer或updater。两处都用纯函数返回回调对象,并区分prop同步、ref和Effect清理的生命周期。
把回调传给 useState(handler) 或 setHandler(nextHandler),还没点击按钮,回调却已经运行;state 中留下的甚至是 undefined。先看传入值是不是函数:React 会将初始化参数中的函数当作 initializer,将 setter 中的函数当作 updater,并保存它们的返回值。
这是函数形式参数的约定,React 19.3.0 仍使用这套规则,不是仅在开发环境才出现的行为。StrictMode 可能额外调用 initializer 或 updater,但关掉它不会让一个裸函数变成普通数据。
初始化和更新都需要包一层
确实需要让 state 保存函数时,传入“返回该函数”的纯函数:
import { useState } from 'react';
export default function ActionButton({ initialHandler, replacementHandler }) {
const [handler, setHandler] = useState(() => initialHandler);
return (
<>
<button onClick={() => handler()}>执行</button>
<button onClick={() => setHandler(() => replacementHandler)}>
换用另一个操作
</button>
</>
);
}
调用方需要提供可调用的两个 handler,并自行处理执行失败。外层 () => initialHandler 只是返回函数对象,不执行其业务;更新时同样用 () => replacementHandler。只修初始化而保留 setHandler(replacementHandler),更新时仍会触发 updater 规则。
如果写成 setHandler(() => replacementHandler()),保存的是调用结果,仍然不是函数对象。不要在 initializer 或 updater 中发送请求、创建订阅或执行清理动作,开发检查和渲染过程不适合承载这些副作用。
初始参数改变不会自动改 state
useState 的初始参数在后续渲染中不会重新初始化 state。上例选择的是首次提供的 handler,之后点击“换用另一个操作”才换成当前渲染中的 replacementHandler。
如果业务就是每次执行最新的 prop,直接调用当前 prop 可能更清楚,不必另存一份 state。如果需要用户选择一项操作并让界面显示这项选择,可以保存操作 ID,再由受控映射取得函数;这也能减少闭包与状态副本之间的关系。
只在命令式流程中保存回调、改变它无需重渲染时,可以评估 ref。ref 更新不会让 UI 自动刷新,不能用它替换本来参与界面选择的 state。
清理函数通常应交给 Effect 返回值
若遇到的是“想把订阅清理函数放进 state”,先检查资源生命周期。Effect 可以直接返回本轮 setup 创建的 cleanup;依赖变化时 React 会先清理上一轮,再建立下一轮,无需等最终卸载,也不必用一次 state 更新再读取旧 cleanup。
资源重跑的判断见StrictMode 下 Effect 的建立与清理。旧闭包读到旧值,则应另外检查定时器的状态快照;包一层函数只改变 setter 的解释方式,不会自动刷新函数内部捕获的变量。
验证初始化、替换与实际触发三个步骤:前两步只保存函数对象,业务操作由事件执行;替换后下一次渲染使用预期 handler。保留 StrictMode,确认用于保存函数的外层纯函数即使重跑,也不会产生业务副作用。


