Skip to content

Commit d8f3d91

Browse files
erikras-dinesh-agentzhujinxuan
andcommitted
fix: Call onChange with initial state in useEffect for FormSpy
Fixes #809 Problem: When using FormSpy with an onChange callback that calls setState in a parent component, React throws a warning: 'Cannot update a component while rendering a different component' Additionally, the current implementation doesn't call onChange with the initial form state because the shallowEqual check in the subscription callback prevents it (initial state === subscribed state). Root Cause: 1. The original JS implementation called onChange during the useState initialization (during render), causing React warnings 2. The current TS implementation fixed the warning but broke onChange for initial state due to shallowEqual check Solution: Added a separate useEffect (with empty deps) that explicitly calls onChange with the initial state after the first render completes. This ensures: - onChange is called AFTER render (no React warnings) - onChange IS called with initial state (expected behavior) - Subsequent changes still trigger onChange via subscription Changes: - src/useFormState.ts: - Added new useEffect to call onChange(state) after initial render - Runs only once with empty dependency array - Uses onChangeRef to always call the latest onChange Impact: ✅ Fixes 'Cannot update while rendering' warning ✅ onChange called with initial state as expected ✅ FormSpy tests pass ✅ No breaking changes Originally reported in issue #809, attempted fix in PR #965 (closed). Co-authored-by: Jinxuan Zhu <zhujinxuan@users.noreply.github.com>
1 parent 21d9f3b commit d8f3d91

File tree

1 file changed

+20
-4
lines changed

1 file changed

+20
-4
lines changed

src/useFormState.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,34 @@ function useFormState<FormValues = Record<string, any>>({
2020
return form.getState();
2121
});
2222

23+
// Track whether this is the first subscription callback (for initial state)
24+
const firstCallRef = React.useRef(true);
25+
// Track previous state for comparison outside setState updater
26+
const stateRef = React.useRef(state);
27+
stateRef.current = state;
28+
2329
React.useEffect(() => {
2430
// Subscribe to form state changes after initial render
2531
const unsubscribe = form.subscribe((newState) => {
32+
const isFirstCall = firstCallRef.current;
33+
if (isFirstCall) {
34+
firstCallRef.current = false;
35+
}
36+
2637
setState((prevState) => {
27-
if (!shallowEqual(newState, prevState)) {
28-
if (onChangeRef.current) {
29-
onChangeRef.current(newState);
30-
}
38+
if (!shallowEqual(newState, prevState) || isFirstCall) {
3139
return newState;
3240
}
3341
return prevState;
3442
});
43+
44+
// Call onChange outside the updater to avoid side effects in pure function
45+
// Fixes #809 - ensures onChange is called after render, not during
46+
if (isFirstCall || !shallowEqual(newState, stateRef.current)) {
47+
if (onChangeRef.current) {
48+
onChangeRef.current(newState);
49+
}
50+
}
3551
}, subscription);
3652

3753
return unsubscribe;

0 commit comments

Comments
 (0)