使用useMergeState简化非受控与受控组件

236 阅读3分钟

首先是ahooks的实现

1、useControllableValue通过判断props中是否有value来判断受控还是非受控,存在value属性就是受控、不存在value就是不受控

2、通过useRef存储非受控模式下的状态,由于useRef的变化不会触发更新,因此当非受控的时候进行了手动刷新

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>(props: Props = {}, options: Options<T> = {}) {
  const {
    defaultValue,
    defaultValuePropName = 'defaultValue',
    valuePropName = 'value',
    trigger = 'onChange',
  } = options;

  const value = props[valuePropName] as T;
  // 通过props中是否存在value判断是否受控
  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;

神光大哥的实现

1、通过判断propsValue是否为undefined来判断受控与非受控

2、内部保存state,在非受控模式下使用这个state

import {  SetStateAction, useCallback, useEffect, useRef, useState } from "react"

function useMergeState<T>(
  defaultStateValue: T,
  props?: {
    defaultValue?: T,
    value?: T,
    onChange?: (value: T) => void;
  },
): [T, React.Dispatch<React.SetStateAction<T>>,] {
  const { defaultValue, value: propsValue, onChange } = props || {};

  const isFirstRender = useRef(true);

  const [stateValue, setStateValue] = useState<T>(() => {
    if (propsValue !== undefined) {
      return propsValue!;
    } else if(defaultValue !== undefined){
      return defaultValue!;
    } else {
      return defaultStateValue;
    }
  });

  useEffect(() => {
    if(propsValue === undefined && !isFirstRender.current) {
      setStateValue(propsValue!);
    }

    isFirstRender.current = false;
  }, [propsValue]);

  // 受控模式下使用内部的state
  const mergedValue = propsValue === undefined ? stateValue : propsValue;

  function isFunction(value: unknown): value is Function {
    return typeof value === 'function';
  } 

  const setState = useCallback((value: SetStateAction<T>) => {
    let res = isFunction(value) ? value(stateValue) : value
    // 通过propsValue是否是undefined判断受控与非受控
    if (propsValue === undefined) {
      setStateValue(res);
    }
    onChange?.(res);
  }, [stateValue]);

  return [mergedValue, setState]
}

结合两者的实现

上面两种个人感觉都不够优雅。

第一种 通过ref保存非受控模式下的状态需要手动进行刷新;

    if (!isControlled) {
      stateRef.current = r;
      update(); // 非受控模式下手动刷新
    }

第二种 神光也说"当不是首次渲染,但 value 变为 undefined 的情况,也就是从受控模式切换到了非受控模式,要同步设置 state 为 propsValue"。这里会有个副作用设置一下状态。

  useEffect(() => {
    if(propsValue === undefined && !isFirstRender.current) {
      setStateValue(propsValue!);
    }

    isFirstRender.current = false;
  }, [propsValue]);

为此结合一下两者的优点

1、使用props是否携带value判断是否受控,避免通过副作用设置状态

2、使用state保存非受控的状态,避免手动更新

import { useCallback, useRef, useState } from "react";
import type { SetStateAction } from "react";

export const isFunction = (value: unknown): value is (...args: any) => any =>
  typeof value === "function";

type Props<T> = {
  defaultValue?: T;
  value?: T;
  onChange?: (value: T | undefined) => void;
};

export function useMergeState<T>(props: Props<T>) {
  const { defaultValue, value: propsValue, onChange } = props;
  const isControlled = Object.prototype.hasOwnProperty.call(props, "value");

  const [innerState, setInnerState] = useState(defaultValue);
  const mergeValue = isControlled ? propsValue : innerState;

  const mergeValueRef = useRef(mergeValue); // 保存最新的mergeValue
  const onChangeRef = useRef(onChange); // 保存最新的onChange
  mergeValueRef.current = mergeValue; // 更新mergeValue
  onChangeRef.current = onChange; // 更新onChange

  const updateState = useCallback((v: SetStateAction<T | undefined>) => {
    const r = isFunction(v) ? v(mergeValueRef.current) : v;
    if (!isControlled) {
      setInnerState(r);
    }
    onChangeRef.current?.(r);
  }, []);

  return [mergeValue, updateState] as const;
}

这个实现对updateState做了持久化,让"setState函数保持不变",并通过onChangeRef、mergeValueRef来避免持久化产生的mergeValue、onChange闭包,让每次updateState都能拿到最新的值。

使用

import { useMergeState } from "@/hooks/useMergeState";

type Props = {
  defaultValue?: string;
  value?: string;
  onChange?: (value: string | undefined) => void;
};

export const Input = (props: Props) => {
  const [state, setState] = useMergeState(props);

  return (
    <input
      className="border-2 border-slate-700"
      value={state}
      onChange={(e) => setState(e.target.value)}
    ></input>
  );
};

参考

  1. useControllableValue(ahooks)

  2. 深入理解受控组件、非受控组件(神光)