用于管理受控属性的hooks,当外部传入对应的受控属性时,将由外部受控属性管理,否则由组件内部自己管理
源码
函数签名
useControllableValue
使用了两个函数签名:
tsx
function useControllableValue<T = any>(
props: StandardProps<T>,
): [T, (v: SetStateAction<T>) => void];
function useControllableValue<T = any>(
props?: Props,
options?: Options<T>,
): [T, (v: SetStateAction<T>, ...args: any[]) => void];
目的是为了在调用时给予开发者更好的类型提示(函数重载):


里面接收一个props和options,我们来看这两个参数的类型声明:
Props
props的类型有两种情况,根据上面的函数签名可知:
- 当只有一个参数时,接收
value
,defaultValue
和onChange
分别对应受控属性,默认值和监听函数 - 当传入options时,props由于属性名不确定,就变成了
Record<string, any>
了
tsx
export type Props = Record<string, any>;
export interface StandardProps<T> {
value: T;
defaultValue?: T;
onChange: (val: T) => void;
}
Options
options参数接收四个可选字段,其中defaultValue
用于指定受控属性的默认值,它的优先级比较低
defautValuePropName
,valuePropName
和trigger
字段分别用于指定外部应该传入的受控属性默认值字段的字段名,受控属性字段的字段名和监听函数字段的字段名:
tsx
export interface Options<T> {
defaultValue?: T;
defaultValuePropName?: string;
valuePropName?: string;
trigger?: string;
}
举个例子,当我们想要封装的组件是一个input
组件时,它的受控属性和触发函数分别为value
和onChange
,这个时候我们直接将props
传入即可:
tsx
interface CustomInputProps {
value?: any;
onChange?: (value: any) => void;
}
const CustomInput = (props: CustomInputProps) => {
const [value, onChange] = useControllableValue(props);
...
}
但当我们想封装一个自定义的受控属性时,我们想指定不同的受控属性名称和监听函数名称,这时候我们就要用到options属性了:
tsx
interface CustomComponentProps {
selectedValues?: string[];
defaultSelectedValues?: string[];
onSelectedValueChange?: (selectedValues: string[]) => void;
}
const CustomComponent = (props: CustomComponentProps) => {
const [value, onChange] = useControllableValue(props, {
// 分别指定三个属性的字段名和传入的字段名一一对应
defaultValuePropName: "defaultSelectedValues",
valuePropName: "selectedValues",
trigger: "onSelectedValueChange",
// 可能还需要给一个默认值,防止后面的逻辑报错
defaultValue: []
});
...
}
受控属性的处理
首先是对props和options两个参数的处理,从这里可以看出和受控属性相关的三个名分别为defaultValue
, value
和onChange
tsx
const props = defaultProps ?? {};
const {
defaultValue,
defaultValuePropName = 'defaultValue',
valuePropName = 'value',
trigger = 'onChange',
} = options;
然后是取出value
相关字段生成默认值:
首先是根据valuePropName
取出外部传入的value
接着根据是否传入value判断此时组件是否受控isControlled
接下来是生成initialValue
,这里可以看到外部传入的受控的value
的优先级最高,其次是外部传入的defaultValue
,最后才是options
传入的defaultValue
值
最后是通过useRef
生成最终要使用的state
tsx
const value = props[valuePropName] as T;
const isControlled = Object.prototype.hasOwnProperty.call(props, valuePropName);
const initialValue = useMemo(() => {
if (isControlled) {
return value;
}
if (Object.prototype.hasOwnProperty.call(props, defaultValuePropName)) {
return props[defaultValuePropName];
}
return defaultValue;
}, []);
const stateRef = useRef(initialValue);
if (isControlled) {
stateRef.current = value;
}
监听函数的处理
这里用到了另一个hook, useUpdate
它是一个强制组件重新渲染的hook
看源码可知重写了的setState
方法,不仅第一个参数可以接受一个函数,还可以接收其它的剩余参数
首先进行参数归一,判断传入的第一个参数是否是函数,如果是则调用这个函数且传入最新的state返回一个值,如果不是直接使用这个传入的参数
接下来判断当前是否是受控模式,如果不是受控模式,则将值赋值给stateRef
,并调用update
函数重新渲染组件
如果是受控模式,则将值返回给外部传入的监听函数,让外部直接更新state:
tsx
const update = useUpdate();
function setState(v: SetStateAction<T>, ...args: any[]) {
const r = isFunction(v) ? v(stateRef.current) : v;
if (!isControlled) {
stateRef.current = r;
update();
}
if (props[trigger]) {
props[trigger](r, ...args);
}
}
整体源码
tsx
import { useMemo, useRef } from 'react';
import type { SetStateAction } from 'react';
import { isFunction } from '../utils';
import useMemoizedFn from '../useMemoizedFn';
import useUpdate from '../useUpdate';
export interface Options<T> {
defaultValue?: T;
defaultValuePropName?: string;
valuePropName?: string;
trigger?: string;
}
export type Props = Record<string, any>;
export interface StandardProps<T> {
value: T;
defaultValue?: T;
onChange: (val: T) => void;
}
function useControllableValue<T = any>(
props: StandardProps<T>,
): [T, (v: SetStateAction<T>) => void];
function useControllableValue<T = any>(
props?: Props,
options?: Options<T>,
): [T, (v: SetStateAction<T>, ...args: any[]) => void];
function useControllableValue<T = any>(defaultProps: Props, options: Options<T> = {}) {
const props = defaultProps ?? {};
const {
defaultValue,
defaultValuePropName = 'defaultValue',
valuePropName = 'value',
trigger = 'onChange',
} = options;
const value = props[valuePropName] as T;
const isControlled = Object.prototype.hasOwnProperty.call(props, valuePropName);
const initialValue = useMemo(() => {
if (isControlled) {
return value;
}
if (Object.prototype.hasOwnProperty.call(props, defaultValuePropName)) {
return props[defaultValuePropName];
}
return defaultValue;
}, []);
const stateRef = useRef(initialValue);
if (isControlled) {
stateRef.current = value;
}
const update = useUpdate();
function setState(v: SetStateAction<T>, ...args: any[]) {
const r = isFunction(v) ? v(stateRef.current) : v;
if (!isControlled) {
stateRef.current = r;
update();
}
if (props[trigger]) {
props[trigger](r, ...args);
}
}
return [stateRef.current, useMemoizedFn(setState)] as const;
}
export default useControllableValue;