[ahooks] useControllableValue源码阅读

用于管理受控属性的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, defaultValueonChange分别对应受控属性,默认值和监听函数
  • 当传入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用于指定受控属性的默认值,它的优先级比较低

defautValuePropNamevaluePropNametrigger字段分别用于指定外部应该传入的受控属性默认值字段的字段名,受控属性字段的字段名和监听函数字段的字段名:

tsx 复制代码
export interface Options<T> {
  defaultValue?: T;
  defaultValuePropName?: string;
  valuePropName?: string;
  trigger?: string;
}

举个例子,当我们想要封装的组件是一个input组件时,它的受控属性和触发函数分别为valueonChange,这个时候我们直接将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, valueonChange

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;
相关推荐
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端