首先是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>
);
};