Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 39 additions & 10 deletions src/useFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,32 @@ function useFormState<FormValues = Record<string, any>>({
const onChangeRef = React.useRef(onChange);
onChangeRef.current = onChange;

// Initialize state with current form state without callbacks
const [state, setState] = React.useState<FormState<FormValues>>(() => {
// Get initial state synchronously but without callbacks
return form.getState();
});
// Initialize with current form state WITHOUT triggering callbacks during render.
// We intentionally use getState() here so render-prop consumers (e.g. <FormSpy>{...})
// can read a fully-populated initial state on first render.
const [state, setState] = React.useState<FormState<FormValues>>(() =>
form.getState(),
);

// We want `onChange` to be called AFTER render (fixes #809) and only with the
// subscription-filtered state.
const firstSubscriptionRef = React.useRef(true);
const pendingOnChangeRef = React.useRef<FormState<FormValues> | null>(null);
const lastOnChangeRef = React.useRef<FormState<FormValues> | null>(null);

React.useEffect(() => {
// Subscribe to form state changes after initial render
const unsubscribe = form.subscribe((newState) => {
// Ensure we set state at least once from the subscription, even if equal,
// so that `onChange` can be fired from an effect after the first render.
const isFirst = firstSubscriptionRef.current;
if (isFirst) {
firstSubscriptionRef.current = false;
}

pendingOnChangeRef.current = newState;

setState((prevState) => {
if (!shallowEqual(newState, prevState)) {
if (onChangeRef.current) {
onChangeRef.current(newState);
}
if (isFirst || !shallowEqual(newState, prevState)) {
return newState;
}
return prevState;
Expand All @@ -38,6 +50,23 @@ function useFormState<FormValues = Record<string, any>>({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

React.useEffect(() => {
const pending = pendingOnChangeRef.current;
if (!pending || !onChangeRef.current) {
return;
}

// Only fire when the subscription has produced a new state and it differs
// from what we've already emitted.
if (lastOnChangeRef.current === null || !shallowEqual(pending, lastOnChangeRef.current)) {
onChangeRef.current(pending);
lastOnChangeRef.current = pending;
}

// Clear pending once we've handled it.
pendingOnChangeRef.current = null;
}, [state]);

const lazyState = {};
addLazyFormState(lazyState, state);
return lazyState as FormState<FormValues>;
Expand Down