Last active
October 21, 2022 05:48
-
-
Save nanxiaobei/b566d62ee58fb43363526e3ffd66a217 to your computer and use it in GitHub Desktop.
同时处理受控与非受控组件,且内部 state 与 onChange 传出 value 不同步的 hook
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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