fix(map): prevent controllable state loops
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
import { act, renderHook } from "@testing-library/react";
|
||||||
|
|
||||||
|
import { useControllableObjectState } from "./useControllableState";
|
||||||
|
|
||||||
|
describe("useControllableObjectState", () => {
|
||||||
|
it("keeps controlled setters stable when the external value changes", () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const { result, rerender } = renderHook(
|
||||||
|
({ value }: { value: { count: number } }) =>
|
||||||
|
useControllableObjectState(value, onChange, { count: 0 }),
|
||||||
|
{
|
||||||
|
initialProps: { value: { count: 0 } },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialSetValue = result.current[1];
|
||||||
|
const initialSetField = result.current[2];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current[2]("count", 1);
|
||||||
|
});
|
||||||
|
expect(onChange).toHaveBeenCalledWith({ count: 1 });
|
||||||
|
|
||||||
|
rerender({ value: { count: 1 } });
|
||||||
|
|
||||||
|
expect(result.current[1]).toBe(initialSetValue);
|
||||||
|
expect(result.current[2]).toBe(initialSetField);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not publish unchanged object fields", () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useControllableObjectState({ count: 1 }, onChange, { count: 0 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current[2]("count", 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onChange).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,10 +10,14 @@ export const useControllableState = <T,>(
|
|||||||
const [internalValue, setInternalValue] = useState<T>(defaultValue);
|
const [internalValue, setInternalValue] = useState<T>(defaultValue);
|
||||||
const value = externalValue !== undefined ? externalValue : internalValue;
|
const value = externalValue !== undefined ? externalValue : internalValue;
|
||||||
const valueRef = useRef(value);
|
const valueRef = useRef(value);
|
||||||
|
const isControlledRef = useRef(externalValue !== undefined);
|
||||||
|
const onExternalChangeRef = useRef(onExternalChange);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
valueRef.current = value;
|
valueRef.current = value;
|
||||||
}, [value]);
|
isControlledRef.current = externalValue !== undefined;
|
||||||
|
onExternalChangeRef.current = onExternalChange;
|
||||||
|
}, [externalValue, onExternalChange, value]);
|
||||||
|
|
||||||
const setValue = useCallback(
|
const setValue = useCallback(
|
||||||
(next: T | ((previous: T) => T)) => {
|
(next: T | ((previous: T) => T)) => {
|
||||||
@@ -21,13 +25,18 @@ export const useControllableState = <T,>(
|
|||||||
typeof next === "function"
|
typeof next === "function"
|
||||||
? (next as (previous: T) => T)(valueRef.current)
|
? (next as (previous: T) => T)(valueRef.current)
|
||||||
: next;
|
: next;
|
||||||
|
|
||||||
|
if (Object.is(valueRef.current, nextValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
valueRef.current = nextValue;
|
valueRef.current = nextValue;
|
||||||
if (externalValue === undefined) {
|
if (!isControlledRef.current) {
|
||||||
setInternalValue(nextValue);
|
setInternalValue(nextValue);
|
||||||
}
|
}
|
||||||
onExternalChange?.(nextValue);
|
onExternalChangeRef.current?.(nextValue);
|
||||||
},
|
},
|
||||||
[externalValue, onExternalChange],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
return [value, setValue] as const;
|
return [value, setValue] as const;
|
||||||
@@ -46,7 +55,11 @@ export const useControllableObjectState = <T extends object>(
|
|||||||
|
|
||||||
const setField = useCallback(
|
const setField = useCallback(
|
||||||
<K extends keyof T>(key: K, nextValue: T[K]) => {
|
<K extends keyof T>(key: K, nextValue: T[K]) => {
|
||||||
setValue((previous) => ({ ...previous, [key]: nextValue }));
|
setValue((previous) =>
|
||||||
|
Object.is(previous[key], nextValue)
|
||||||
|
? previous
|
||||||
|
: { ...previous, [key]: nextValue },
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[setValue],
|
[setValue],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user