Cadenza
EN

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 更新函数,且引用恒定,可以安全放进依赖数组;
  • valueundefined 即受控: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 的行为。

库内的使用点

位置受控的状态
DataTable选择便利层的 value / defaultValue / onChange(跨页存档)
InfiniteSelect / InfiniteCombobox选择 ids;弹层 isOpen、搜索词 inputValue
DataPaginationpagelimit 各自独立受控