Skip to content

Commit d1a7b99

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 d1a7b99

File tree

1 file changed

+16
-1
lines changed

1 file changed

+16
-1
lines changed

src/useFormState.ts

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

23+
// Track previous state for comparison in subscription callback
24+
const prevStateRef = React.useRef<FormState<FormValues> | null>(null);
25+
2326
React.useEffect(() => {
2427
// Subscribe to form state changes after initial render
2528
const unsubscribe = form.subscribe((newState) => {
2629
setState((prevState) => {
2730
if (!shallowEqual(newState, prevState)) {
28-
if (onChangeRef.current) {
31+
// Call onChange for per-update notifications (after initial render)
32+
// Only call if we've already reported initial state
33+
if (prevStateRef.current !== null && onChangeRef.current) {
2934
onChangeRef.current(newState);
3035
}
3136
return newState;
@@ -38,6 +43,16 @@ function useFormState<FormValues = Record<string, any>>({
3843
// eslint-disable-next-line react-hooks/exhaustive-deps
3944
}, []);
4045

46+
// Call onChange with initial state after first render
47+
// This fixes #809 - ensures onChange is called after render, not during
48+
React.useEffect(() => {
49+
// Only call for initial state (when prevStateRef is null)
50+
if (prevStateRef.current === null && onChangeRef.current) {
51+
onChangeRef.current(state);
52+
}
53+
prevStateRef.current = state;
54+
}, [state]);
55+
4156
const lazyState = {};
4257
addLazyFormState(lazyState, state);
4358
return lazyState as FormState<FormValues>;

0 commit comments

Comments
 (0)