useControllableState
useState 形状的受控/非受控二态 hook —— @gedatou/cadenza-utils
@gedatou/cadenza-utils 是配套的工具包,目前的核心是 useControllableState:
把 ahooks 的 useControllableValue 收敛成 useState 的形状,让一个组件
同时支持受控与非受控两种用法——库里所有组件的受控实现都建立在它上面。
pnpm add @gedatou/cadenza-utils契约
const [state, setState] = useControllableState({ value, defaultValue, onChange, fallback })- 返回
[state, setState],setState是标准的Dispatch<SetStateAction<T>>: 接受值或(prev) => next更新函数,且引用恒定,可以安全放进依赖数组; value非undefined即受控:state 跟随 prop,setState只触发onChange,内部状态不动——与 React 原生输入框的受控约定一致;- 否则为非受控:hook 自持状态,
defaultValue是初始值,onChange照样触发; fallback兜底"非受控且没给 defaultValue"的情形,让返回类型收窄为T而不是T | undefined。
示例
同一个 Stepper 组件,传 value 即受控、只传 defaultValue 即非受控:
为什么二次封装 ahooks
useControllableValue 本身能干这活,但它的 API 是「props 对象 + 字符串
propName 配置 + setter 带变长参数」,和 useState 长得完全不一样。这层封装
把它压回 useState 的形状,调用处零学习成本、可直接替换既有的 useState。
受控性在首渲染锁定(Base UI 语义)
受控性由首渲染的 value !== undefined 判定,此后不再改判——undefined
专属「非受控」,受控的空值用 null(库内 DataTable / InfiniteSelect 的
单选清空都传 null)。中途切换受控性、或非受控中途改 defaultValue,
开发环境会 console.error 警告,运行时忽略:锁定为受控的组件在 value
变回 undefined 时渲染 undefined,不会退回内部状态。这与
@base-ui/utils/useControlled 同语义,也是原生受控 input 的行为。