({
onChange,
subscription = all,
}: UseFormStateParams<FormValues> = {})
| 7 | import shallowEqual from "./shallowEqual"; |
| 8 | |
| 9 | function useFormState<FormValues = Record<string, any>>({ |
| 10 | onChange, |
| 11 | subscription = all, |
| 12 | }: UseFormStateParams<FormValues> = {}): FormState<FormValues> { |
| 13 | const form: FormApi<FormValues> = useForm<FormValues>("useFormState"); |
| 14 | const onChangeRef = React.useRef(onChange); |
| 15 | onChangeRef.current = onChange; |
| 16 | |
| 17 | // Initialize with current form state WITHOUT triggering callbacks during render. |
| 18 | // We intentionally use getState() here so render-prop consumers (e.g. <FormSpy>{...}) |
| 19 | // can read a fully-populated initial state on first render. |
| 20 | const [state, setState] = React.useState<FormState<FormValues>>(() => |
| 21 | form.getState(), |
| 22 | ); |
| 23 | |
| 24 | // We want `onChange` to be called AFTER render (fixes #809) and only with the |
| 25 | // subscription-filtered state. |
| 26 | const firstSubscriptionRef = React.useRef(true); |
| 27 | const pendingOnChangeRef = React.useRef<FormState<FormValues> | null>(null); |
| 28 | const lastOnChangeRef = React.useRef<FormState<FormValues> | null>(null); |
| 29 | |
| 30 | React.useEffect(() => { |
| 31 | const unsubscribe = form.subscribe((newState) => { |
| 32 | // Ensure we set state at least once from the subscription, even if equal, |
| 33 | // so that `onChange` can be fired from an effect after the first render. |
| 34 | const isFirst = firstSubscriptionRef.current; |
| 35 | if (isFirst) { |
| 36 | firstSubscriptionRef.current = false; |
| 37 | } |
| 38 | |
| 39 | pendingOnChangeRef.current = newState; |
| 40 | |
| 41 | setState((prevState) => { |
| 42 | if (isFirst || !shallowEqual(newState, prevState)) { |
| 43 | return newState; |
| 44 | } |
| 45 | return prevState; |
| 46 | }); |
| 47 | }, subscription); |
| 48 | |
| 49 | return unsubscribe; |
| 50 | // eslint-disable-next-line react-hooks/exhaustive-deps |
| 51 | }, []); |
| 52 | |
| 53 | React.useEffect(() => { |
| 54 | const pending = pendingOnChangeRef.current; |
| 55 | if (!pending || !onChangeRef.current) { |
| 56 | return; |
| 57 | } |
| 58 | |
| 59 | // Only fire when the subscription has produced a new state and it differs |
| 60 | // from what we've already emitted. |
| 61 | if (lastOnChangeRef.current === null || !shallowEqual(pending, lastOnChangeRef.current)) { |
| 62 | onChangeRef.current(pending); |
| 63 | lastOnChangeRef.current = pending; |
| 64 | } |
| 65 | |
| 66 | // Clear pending once we've handled it. |
searching dependent graphs…