Skip to content

Commit 3667469

Browse files
committed
feat(hooks): refactor useControllableValue with useLatestCallback and shallowEqual
- Replace `useLatestRef` with `useLatestCallback` for better callback handling - Use `shallowEqual` from `fast-equals` to compare state changes - Rename internal functions `getValue` to `getState` and `getDefaultValue` to `getDefaultState` - Update type definitions and improve equality checks in state updates - Add `fast-equals` as a dependency in package.json and lockfile
1 parent 79a4eae commit 3667469

File tree

5 files changed

+73
-33
lines changed

5 files changed

+73
-33
lines changed

packages/examples/app/js/hooks/useControllableValue.ts

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
*/
44

55
import useIsMounted from './useIsMounted';
6-
import useLatestRef from './useLatestRef';
6+
import { shallowEqual } from 'fast-equals';
77
import { isFunction } from '/js/utils/utils';
8-
import React, { useCallback, useEffect, useState } from 'react';
8+
import React, { useEffect, useState } from 'react';
9+
import useLatestCallback from './useLatestCallback';
910

1011
export interface Props {
1112
[prop: string]: any;
@@ -54,22 +55,22 @@ function isControlled<V>(props: Props, options: Options<V>): boolean {
5455
}
5556

5657
/**
57-
* @function getValue
58+
* @function getState
5859
* @param props 组件 Props
5960
* @param options 配置选项
6061
*/
61-
function getValue<V>(props: Props, options: Options<V>): V {
62+
function getState<V>(props: Props, options: Options<V>): V {
6263
const valuePropName = getValuePropName(options);
6364

6465
return props[valuePropName];
6566
}
6667

6768
/**
68-
* @function getDefaultValue
69+
* @function getDefaultState
6970
* @param props 组件 Props
7071
* @param options 配置选项
7172
*/
72-
function getDefaultValue<V>(props: Props, options: Options<V>): V {
73+
function getDefaultState<V>(props: Props, options: Options<V>): V {
7374
const defaultValuePropName = getDefaultValuePropName(options);
7475

7576
return props[defaultValuePropName];
@@ -106,49 +107,43 @@ export default function useControllableValue<V = undefined>(
106107
options: Options<V> = {}
107108
): [value: V | undefined, setValue: SetValueAction<V | undefined>] {
108109
const isMounted = useIsMounted();
109-
const propsRef = useLatestRef(props);
110-
const optionsRef = useLatestRef(options);
111110

112-
const [value = options.defaultValue, setState] = useState<V | undefined>(() => {
111+
const [currentState = options.defaultValue, setState] = useState<V | undefined>(() => {
113112
if (isControlled(props, options)) {
114-
return getValue(props, options);
113+
return getState(props, options);
115114
}
116115

117-
return getDefaultValue(props, options);
116+
return getDefaultState(props, options);
118117
});
119118

120-
const valueRef = useLatestRef(value);
121-
122-
const setValue = useCallback((value: React.SetStateAction<V | undefined>, ...args: any[]): void => {
119+
const setValue = useLatestCallback((value: React.SetStateAction<V | undefined>, ...args: unknown[]): void => {
123120
if (isMounted()) {
124-
const { current: prevState } = valueRef;
125-
const state = isFunction(value) ? value(prevState) : value;
121+
const state = currentState ?? options.defaultValue;
122+
const nextState = isFunction(value) ? value(state) : value;
126123

127-
if (state !== prevState) {
128-
const { current: props } = propsRef;
124+
if (!shallowEqual(nextState, state)) {
129125
const { trigger = 'onChange' } = props;
130-
const { current: options } = optionsRef;
131126

132127
if (!isControlled(props, options)) {
133-
setState(state);
128+
setState(nextState);
134129
}
135130

136131
if (isFunction(props[trigger])) {
137-
props[trigger](state, ...args);
132+
props[trigger](nextState, ...args);
138133
}
139134
}
140135
}
141-
}, []);
136+
});
142137

143138
useEffect(() => {
144139
if (isControlled(props, options)) {
145-
const nextValue = getValue(props, options);
140+
const nextState = getState(props, options);
146141

147-
if (nextValue !== value) {
148-
setState(nextValue);
142+
if (!shallowEqual(nextState, currentState)) {
143+
setState(nextState);
149144
}
150145
}
151146
});
152147

153-
return [value, setValue];
148+
return [currentState, setValue];
154149
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* @module useLatestCallback
3+
*/
4+
5+
import { useRef } from 'react';
6+
import useLatestRef from './useLatestRef';
7+
8+
export interface Callback {
9+
(this: unknown, ...args: any[]): any;
10+
}
11+
12+
/**
13+
* @function useLatestCallback
14+
* @description [hook] 持久化可获取最新上下文的回调函数
15+
* @param callback 待处理的回调函数
16+
*/
17+
export default function useLatestCallback<C extends Callback>(callback: C): C {
18+
const callbackRef = useLatestRef(callback);
19+
const latestCallbackRef = useRef<C | null>(null);
20+
21+
if (latestCallbackRef.current == null) {
22+
latestCallbackRef.current = function (this, ...args) {
23+
return callbackRef.current.apply(this, args);
24+
} as C;
25+
}
26+
27+
return latestCallbackRef.current;
28+
}

packages/examples/app/js/hooks/useLatestRef.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,29 @@ import React, { useMemo, useRef } from 'react';
66

77
/**
88
* @function useLatestRef
9-
* @description 生成自更新 useRef 对象
9+
* @description [hook] 生成自更新 useRef 对象
10+
* @param value 引用值
1011
*/
11-
export default function useLatestRef<T = undefined>(): React.RefObject<T | undefined>;
12+
export default function useLatestRef<T>(value: T): React.RefObject<T>;
1213
/**
1314
* @function useLatestRef
14-
* @description 生成自更新 useRef 对象
15+
* @description [hook] 生成自更新 useRef 对象
1516
* @param value 引用值
1617
*/
17-
export default function useLatestRef<T>(value: T): React.RefObject<T>;
18+
export default function useLatestRef<T>(value: T | null): React.RefObject<T | null>;
19+
/**
20+
* @function useLatestRef
21+
* @description [hook] 生成自更新 useRef 对象
22+
* @param value 引用值
23+
*/
24+
export default function useLatestRef<T>(value: T | undefined): React.RefObject<T | undefined>;
1825
/**
1926
* @function useLatestRef
20-
* @description 生成自更新 useRef 对象
27+
* @description [hook] 生成自更新 useRef 对象
2128
* @param value 引用值
2229
*/
23-
export default function useLatestRef<T = undefined>(value?: T): React.RefObject<T | undefined> {
24-
const valueRef = useRef(value);
30+
export default function useLatestRef<T>(value: T | null | undefined): React.RefObject<T | null | undefined> {
31+
const valueRef = useRef<T | null | undefined>(value);
2532

2633
// https://github.com/alibaba/hooks/issues/728
2734
valueRef.current = useMemo(() => value, [value]);

packages/examples/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"chardet": "^2.1.0",
2929
"classnames": "^2.5.1",
3030
"copy-to-clipboard": "^3.3.3",
31+
"fast-equals": "^5.3.2",
3132
"react": "^19.2.0",
3233
"react-dom": "^19.2.0",
3334
"react-error-boundary": "^6.0.0"

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)