Skip to content

Commit 0a71e19

Browse files
committed
feat: add useControlledState hook
1 parent 432d3a6 commit 0a71e19

File tree

2 files changed

+66
-1
lines changed

2 files changed

+66
-1
lines changed

src/hooks/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ export { default as usePersistFn } from './usePersistFn';
1212
export { default as useUpdateEffect } from './useUpdateEffect';
1313
export { default as useForceUpdate } from './useForceUpdate';
1414
export { default as useDeepEffect } from './useDeepEffect';
15+
export { default as useControlledState } from './useControlledState';
1516

17+
// bom
1618
export { default as useInterval } from './useInterval';
1719
export { default as useTimeout } from './useTimeout';
1820
export { default as useTimeoutFn } from './useTimeoutFn';
19-
export { default as useUntil } from './useUntil';
2021

2122
export { default as useSet } from './useSet';
2223
export { default as useMap } from './useMap';
@@ -26,6 +27,7 @@ export { default as useCreation } from './useCreation';
2627
export { default as useAsync } from './useAsync';
2728
export { default as useAsyncFn } from './useAsyncFn';
2829
export { default as useAsyncRetry } from './useAsyncRetry';
30+
export { default as useUntil } from './useUntil';
2931

3032
export { default as useStorageState } from './useStorageState';
3133
export { default as useLocalStorageState } from './useLocalStorageState';

src/hooks/useControlledState.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react';
2+
3+
type DefaultState<T> = T | (() => T);
4+
5+
/**
6+
* 控制state
7+
* @param defaultStateValue {DefaultState} 初始化状态优先级低于 option.defaultValue
8+
* @param option
9+
* @param option.defaultValue {DefaultState} 初始化状态
10+
* @param option.value 使用传入state
11+
* @param option.onChange 状态变更触发
12+
* @param option.postState 外部处理完状态作为返回的state
13+
*/
14+
function useControlledState<T, R = T>(
15+
defaultStateValue: DefaultState<T>,
16+
option?: {
17+
defaultValue?: DefaultState<T>;
18+
value?: T;
19+
onChange?: (value: T, prevValue: T) => void;
20+
postState?: (value: T) => T;
21+
},
22+
): [R, (value: T) => void] {
23+
const { defaultValue, value, onChange, postState } = option || {};
24+
const [innerValue, setInnerValue] = React.useState<T>(() => {
25+
if (value !== undefined) {
26+
return value;
27+
}
28+
if (defaultValue !== undefined) {
29+
return typeof defaultValue === 'function' ? (defaultValue as any)() : defaultValue;
30+
}
31+
return typeof defaultStateValue === 'function'
32+
? (defaultStateValue as any)()
33+
: defaultStateValue;
34+
});
35+
36+
let mergedValue = value !== undefined ? value : innerValue;
37+
if (postState) {
38+
mergedValue = postState(mergedValue);
39+
}
40+
41+
function triggerChange(newValue: T) {
42+
setInnerValue(newValue);
43+
if (mergedValue !== newValue && onChange) {
44+
onChange(newValue, mergedValue);
45+
}
46+
}
47+
48+
const firstRenderRef = React.useRef(true);
49+
React.useEffect(() => {
50+
if (firstRenderRef.current) {
51+
firstRenderRef.current = false;
52+
return;
53+
}
54+
55+
if (value === undefined) {
56+
setInnerValue(value as T);
57+
}
58+
}, [value]);
59+
60+
return [mergedValue as unknown as R, triggerChange];
61+
}
62+
63+
export default useControlledState;

0 commit comments

Comments
 (0)