[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;
相关推荐
zeqinjie几秒前
Flutter 使用 AI Cursor 快速完成一个图表封装【提效】
前端·flutter
真上帝的左手6 分钟前
24. 前端-js框架-Vue
前端·javascript·vue.js
3Katrina16 分钟前
《Stitch的使用指南以及AI新开发模式杂谈》
前端
无羡仙18 分钟前
按下回车后,网页是怎么“跳”出来的?
前端·node.js
喝拿铁写前端18 分钟前
Vue 实战:构建灵活可维护的菜单系统
前端·vue.js·设计模式
ZzMemory21 分钟前
一套通关CSS选择器,玩转元素定位
前端·css·面试
圆心角24 分钟前
小米面挂了
前端·面试
我的小月月26 分钟前
Vue移动端"回到顶部"组件深度解析:拖拽、动画与性能优化实践
前端
拳打南山敬老院26 分钟前
从零构建一个插件系统(六)低代码场景的插件构建思考
javascript·架构
前端康师傅28 分钟前
你还在相信前端加密吗?前端密码加密安全指南
前端·安全