[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;
相关推荐
augenstern4161 小时前
HTML面试题
前端·html
张可1 小时前
一个KMP/CMP项目的组织结构和集成方式
android·前端·kotlin
G等你下课1 小时前
React 路由懒加载入门:提升首屏性能的第一步
前端·react.js·前端框架
谢尔登2 小时前
【React Native】ScrollView 和 FlatList 组件
javascript·react native·react.js
蓝婷儿2 小时前
每天一个前端小知识 Day 27 - WebGL / WebGPU 数据可视化引擎设计与实践
前端·信息可视化·webgl
然我2 小时前
面试官:如何判断元素是否出现过?我:三种哈希方法任你选
前端·javascript·算法
OpenTiny社区3 小时前
告别代码焦虑,单元测试让你代码自信力一路飙升!
前端·github
kk_stoper3 小时前
如何通过API查询实时能源期货价格
java·开发语言·javascript·数据结构·python·能源
pe7er3 小时前
HTTPS:本地开发绕不开的设置指南
前端
晨枫阳3 小时前
前端VUE项目-day1
前端·javascript·vue.js