Skip to content

Instantly share code, notes, and snippets.

@nanxiaobei
Last active October 21, 2022 05:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nanxiaobei/b566d62ee58fb43363526e3ffd66a217 to your computer and use it in GitHub Desktop.
Save nanxiaobei/b566d62ee58fb43363526e3ffd66a217 to your computer and use it in GitHub Desktop.
同时处理受控与非受控组件,且内部 state 与 onChange 传出 value 不同步的 hook
import { SetStateAction, useCallback, useReducer, useRef } from 'react';
// useControlState
// 同时处理受控和非受控组件,但与 useControlValue 不同
// 本 hook 用于此种情况:内部 state 与 onChange 时传出 value 不同步 (如内部 state 情况复杂,超前于 value)
// 此时需同步 value -> state (若 props.value 变化),同时同步 state -> value (若 state 变化且需要传出)
function useControlState<S = any, V = any>({
props = {},
valueToState,
stateToValue,
initialState,
}: {
props: Record<string, any>; // 组件的 props
valueToState: (nextValue: V, prevState: S) => S; // 将 value 转为 state 的函数
stateToValue: (nextState: S, prevValue: V) => V; // 将 state 转为 value 的函数
initialState?: S; // 默认 state
}) {
const isControlled = 'value' in props;
// value & state
const value = useRef<V>(undefined as unknown as V);
const state = useRef<S>(initialState as S);
const hasTouched = useRef(false); // 字段是否被赋值过 (不是一直 undefined)
const hasValueInit = useRef(false); // 字段是否已初始化
// value → state
const syncValueToState = (newValue: V) => {
value.current = newValue;
if (value.current !== undefined) hasTouched.current = true;
state.current = valueToState(value.current, state.current);
};
if (isControlled && value.current !== props.value) {
syncValueToState(props.value);
} else if (!hasValueInit.current) {
syncValueToState(props.defaultValue);
}
hasValueInit.current = true;
// state → value
const [, internalUpdate] = useReducer((s) => !s, false);
const onChangeRef = useRef(props.onChange);
onChangeRef.current = props.onChange;
const stateToValueRef = useRef(stateToValue);
stateToValueRef.current = stateToValue;
const setState = useCallback(
(s: SetStateAction<S>) => {
state.current = s instanceof Function ? s(state.current) : s;
const prevVal = value.current;
value.current = stateToValueRef.current(state.current, value.current);
// value: {}
if (value.current !== undefined) {
hasTouched.current = true;
onChangeRef.current?.(value.current); // uncontrolled(notify) || controlled(notify & update)
if (!isControlled) internalUpdate(); // uncontrolled(update)
return;
}
// value: undefined
if (hasTouched.current) {
onChangeRef.current?.(value.current); // uncontrolled(notify) || controlled(notify & update?)
// // uncontrolled(update) || controlled(if undefined x 2, onChange -> no update)
if (!isControlled || prevVal === undefined) internalUpdate();
return;
}
internalUpdate();
},
[isControlled]
);
return [state.current, setState] as const;
}
export default useControlState;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment