[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;
相关推荐
yqcoder2 分钟前
vue2 和 vue3 生命周期的区别
前端·javascript·vue.js
excel13 分钟前
前端人必备的 JavaScript API 全面指南(含 postMessage、File、Stream、Web 组件等)
前端
m0_738120725 小时前
CTFshow系列——命令执行web53-56
前端·安全·web安全·网络安全·ctfshow
Liu.7747 小时前
uniappx鸿蒙适配
前端
山有木兮木有枝_8 小时前
从代码到创作:探索AI图片生成的神奇世界
前端·coze
ZXT8 小时前
js基础重点复习
javascript
言兴9 小时前
秋招面试---性能优化(良子大胃袋)
前端·javascript·面试
WebInfra10 小时前
Rspack 1.5 发布:十大新特性速览
前端·javascript·github
雾恋10 小时前
我用 trae 写了一个菜谱小程序(灶搭子)
前端·javascript·uni-app
烛阴11 小时前
TypeScript 中的 `&` 运算符:从入门、踩坑到最佳实践
前端·javascript·typescript