From 671e05e6fdaad20a0006feef536161b5b63b0d3f Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Mon, 7 Nov 2022 19:36:37 +0900 Subject: [PATCH] examples(RHF): fix useFormSync() handle dynamic routes properly (#140) * examples(RHF): fix useFormSync() handle dynamic routes properly * examples(RHF): export types & remove unnecessary imports * examples(RHF): remove unused imports * examples(RHF): revert unintended change --- .../react-hook-form/pages/form/[index].tsx | 119 ++++++++++++++---- .../react-hook-form/pages/form/success.tsx | 12 +- examples/react-hook-form/pages/index.tsx | 12 +- .../react-hook-form/src/hooks/useFormSync.ts | 77 ++++++++---- 4 files changed, 168 insertions(+), 52 deletions(-) diff --git a/examples/react-hook-form/pages/form/[index].tsx b/examples/react-hook-form/pages/form/[index].tsx index f0730e88..1277bc53 100755 --- a/examples/react-hook-form/pages/form/[index].tsx +++ b/examples/react-hook-form/pages/form/[index].tsx @@ -1,10 +1,12 @@ +import { useEffect, useRef } from 'react' import type { GetServerSideProps, NextPage } from 'next' import Head from 'next/head' +import Link from 'next/link' import { useRouter } from 'next/router' import { SubmitHandler } from 'react-hook-form/dist/types/form' import { syncEffect } from 'recoil-sync' import { array, object, string } from '@recoiljs/refine' -import { initializableAtom } from 'recoil-sync-next' +import { initializableAtomFamily } from 'recoil-sync-next' import { useFormSync } from '../../src/hooks/useFormSync' import styles from '../../styles/form.module.css' @@ -15,13 +17,13 @@ type FormState = { selectedRadio: string selectedCheckbox: readonly string[] items: readonly { - value: string radio: string checkbox: string + text: string }[] } -const formState = initializableAtom({ +const formState = initializableAtomFamily({ key: 'formState', effects: [ syncEffect({ @@ -33,9 +35,9 @@ const formState = initializableAtom({ selectedCheckbox: array(string()), items: array( object({ - value: string(), radio: string(), checkbox: string(), + text: string(), }) ), }), @@ -43,16 +45,27 @@ const formState = initializableAtom({ ], }) +type PageProps = { + index: string + defaultValues: FormState +} + let count = 1 const createNewItem = (): FormState['items'][number] => ({ - value: '', radio: `${count}`, checkbox: `${count++}`, + text: '', }) -const Form: NextPage = (props) => { +const Form: NextPage = ({ index, defaultValues }) => { // check render - console.log('Form: re render') + const renderRef = useRef(true) + if (renderRef.current) { + renderRef.current = false + console.log(`Form[${index}]: initial render`) + } else { + console.log(`Form[${index}]: re render`) + } const { control, @@ -61,14 +74,28 @@ const Form: NextPage = (props) => { onChangeForm, handleSubmit, reset, + resetFormOnly, useFieldArraySync, - } = useFormSync(formState(props)) + } = useFormSync(formState(index, defaultValues)) const { fields, append, prepend, remove, swap, move, insert } = useFieldArraySync({ control, name: 'items', }) + /* + * Next.js dynamic routes does not unmount page components + * when only slugs are changed (just re-rendering). + * Therefore, uncontrolled input elements are not updated + * from the previous page (i.e. slug). + * To solve this problem, when slugs ('index' in this case) change, + * reset the RHF form state to reflect the Recoil state. + * Unlike reset(), resetFormOnly() doesn't reset the Recoil state. + */ + useEffect(() => { + resetFormOnly() + }, [index, resetFormOnly]) + const router = useRouter() const onSubmit: SubmitHandler = async (data) => { console.log('submit data', data) @@ -78,12 +105,12 @@ const Form: NextPage = (props) => { return (
- Form + Form[{index}]
-

Form

+

Form[{index}]

@@ -124,7 +151,7 @@ const Form: NextPage = (props) => { />
- - +
+ +
+
+ + +
+
+
+ Form[1] +
+
+ Form[2] +
+
+ Form[3] +
+
) @@ -172,20 +229,32 @@ const Form: NextPage = (props) => { export default Form -export const getServerSideProps: GetServerSideProps = async ({ +export const getServerSideProps: GetServerSideProps = async ({ params, }) => { + console.log(`Form[${params?.index}]: executing gSSP`) return { props: { - name: 'a', - comment: 'b', - time: new Date().toLocaleTimeString(), - selectedRadio: 'A', - selectedCheckbox: ['A'], - items: [ - { value: '', radio: 'A', checkbox: 'A' }, - { value: '', radio: 'B', checkbox: 'B' }, - ], + index: params?.index as string, + defaultValues: { + name: 'a', + comment: 'b', + time: new Date().toLocaleTimeString(), + selectedRadio: 'A', + selectedCheckbox: ['A'], + items: [ + { + radio: 'A', + checkbox: 'A', + text: '', + }, + { + radio: 'B', + checkbox: 'B', + text: '', + }, + ], + }, }, } } diff --git a/examples/react-hook-form/pages/form/success.tsx b/examples/react-hook-form/pages/form/success.tsx index 50782205..dd27807d 100755 --- a/examples/react-hook-form/pages/form/success.tsx +++ b/examples/react-hook-form/pages/form/success.tsx @@ -14,7 +14,17 @@ const SubmitSuccess: NextPage = () => {

Submit success!!!

- form +
+
+ Form[1] +
+
+ Form[2] +
+
+ Form[3] +
+
) diff --git a/examples/react-hook-form/pages/index.tsx b/examples/react-hook-form/pages/index.tsx index 5e08d2cd..9ee1c51a 100755 --- a/examples/react-hook-form/pages/index.tsx +++ b/examples/react-hook-form/pages/index.tsx @@ -14,7 +14,17 @@ const Home: NextPage = () => {

Top page

- form +
+
+ Form[1] +
+
+ Form[2] +
+
+ Form[3] +
+
) diff --git a/examples/react-hook-form/src/hooks/useFormSync.ts b/examples/react-hook-form/src/hooks/useFormSync.ts index 1b479074..377e8cff 100644 --- a/examples/react-hook-form/src/hooks/useFormSync.ts +++ b/examples/react-hook-form/src/hooks/useFormSync.ts @@ -1,11 +1,10 @@ -import { useCallback, useRef } from 'react' +import { useCallback } from 'react' import { DeepPartial, FieldPath, FieldValues, - InternalFieldName, + KeepStateOptions, RegisterOptions, - Resolver, useFieldArray, UseFieldArrayAppend, UseFieldArrayInsert, @@ -21,6 +20,7 @@ import { UseFormProps, UseFormRegister, UseFormRegisterReturn, + UseFormReset, UseFormReturn, } from 'react-hook-form' import { @@ -30,7 +30,7 @@ import { useSetRecoilState, } from 'recoil' -type RegisterWithDefaultChecked< +export type RegisterWithDefaultChecked< TFieldValues extends FieldValues, TContext = any > = = FieldPath>( @@ -41,12 +41,17 @@ type RegisterWithDefaultChecked< options?: RegisterOptions ) => UseFormRegisterReturn -type UseFormSyncReturn< +export type ResetFormOnly = ( + keepStateOptions?: KeepStateOptions +) => void + +export type UseFormSyncReturn< TFieldValues extends FieldValues, TContext = any > = UseFormReturn & { registerWithDefaultValue: UseFormRegister registerWithDefaultChecked: RegisterWithDefaultChecked + resetFormOnly: ResetFormOnly onChangeForm: () => void useFieldArraySync: ( props: UseFieldArrayProps @@ -66,16 +71,17 @@ export function useFormSync( } return formLoadable.contents }, - [] + [formState] ) - const defaultValuesRef = useRef() - defaultValuesRef.current ??= getDefaultValues() - const getDefaultValue = (name: string) => { - return name - .split('.') - .reduce((value, segment) => value?.[segment], defaultValuesRef.current!) - } + const getDefaultValue = useCallback( + (name: string) => { + return name + .split('.') + .reduce((value, segment) => value?.[segment], getDefaultValues()) + }, + [getDefaultValues] + ) const { register, @@ -86,15 +92,15 @@ export function useFormSync( ...props, // `DeepPartial` is expected to be removed in the future // https://github.com/react-hook-form/react-hook-form/issues/8510#issuecomment-1157129666 - defaultValues: defaultValuesRef.current as DeepPartial, + defaultValues: getDefaultValues() as DeepPartial, }) const registerWithDefaultValue: UseFormRegister = useCallback( (name, options) => { - const defaultValue = getDefaultValue(name) ?? null + const defaultValue = getDefaultValue(name) return { ...register(name, options), defaultValue } }, - [register] + [getDefaultValue, register] ) const registerWithDefaultChecked: RegisterWithDefaultChecked< @@ -111,19 +117,39 @@ export function useFormSync( : defaultValue === value return { ...register(name, options), value, defaultChecked } }, - [register] + [getDefaultValue, register] ) - const resetState = useResetRecoilState(formState) - const reset = useCallback(() => { - resetState() - resetForm(structuredClone(getDefaultValues())) - }, [getDefaultValues, resetForm, resetState]) + const setFormState = useSetRecoilState(formState) + const resetFormState = useResetRecoilState(formState) + + const reset: UseFormReset = useCallback( + (values, keepStateOptions) => { + let newValues: TFieldValues | undefined + if (typeof values === 'function') { + newValues = values(getValues()) + setFormState(newValues) + } else if (values) { + newValues = values as TFieldValues + setFormState(newValues) + } else { + resetFormState() + newValues = structuredClone(getDefaultValues()) + } + resetForm(newValues, keepStateOptions) + }, + [getDefaultValues, getValues, resetForm, resetFormState, setFormState] + ) + const resetFormOnly: ResetFormOnly = useCallback( + (keepStateOptions) => { + resetForm(structuredClone(getDefaultValues()), keepStateOptions) + }, + [getDefaultValues, resetForm] + ) - const setFormValues = useSetRecoilState(formState) const onChangeForm = useCallback(() => { - setFormValues(structuredClone(getValues())) - }, [setFormValues, getValues]) + setFormState(structuredClone(getValues())) + }, [setFormState, getValues]) const useFieldArraySync = ( props: UseFieldArrayProps @@ -186,6 +212,7 @@ export function useFormSync( registerWithDefaultChecked, getValues, reset, + resetFormOnly, onChangeForm, useFieldArraySync, }