From a06f5a4671ff9a0be290059e7022364250c15fe1 Mon Sep 17 00:00:00 2001 From: Kacper Polak Date: Mon, 17 Mar 2025 20:57:27 +0100 Subject: [PATCH 1/3] feat(react-form): correctly handle client-side validation with server-action --- .../src/app/client-component.tsx | 2 +- packages/react-form/src/useForm.tsx | 27 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/examples/react/next-server-actions/src/app/client-component.tsx b/examples/react/next-server-actions/src/app/client-component.tsx index b7e62909c..9ac4d21e8 100644 --- a/examples/react/next-server-actions/src/app/client-component.tsx +++ b/examples/react/next-server-actions/src/app/client-component.tsx @@ -21,7 +21,7 @@ export const ClientComp = () => { const formErrors = useStore(form.store, (formState) => formState.errors) return ( -
form.handleSubmit()}> + {formErrors.map((error) => (

{error}

))} diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 2831190d1..0d2fb8f78 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -11,7 +11,7 @@ import type { FormState, FormValidateOrFn, } from '@tanstack/form-core' -import type { PropsWithChildren, ReactNode } from 'react' +import type { FormEventHandler, PropsWithChildren, ReactNode } from 'react' import type { FieldComponent } from './useField' import type { NoInfer } from '@tanstack/react-store' @@ -80,6 +80,12 @@ export interface ReactFormApi< ) => TSelected children: ((state: NoInfer) => ReactNode) | ReactNode }) => ReactNode + /** + * A function that handles form submissions with server-actions support. Internally calls `handleSubmit` method, but additionally takes care of client-side validation. + */ + handleActionSubmit: ( + submitMeta?: TSubmitMeta, + ) => FormEventHandler } /** @@ -164,6 +170,7 @@ export function useForm< TSubmitMeta >, ) { + const isActionSubmittedRef = React.useRef(false) const [formApi] = useState(() => { const api = new FormApi< TFormData, @@ -202,6 +209,24 @@ export function useForm< /> ) } + extendedApi.handleActionSubmit = (submitMeta) => { + return async (event) => { + if (isActionSubmittedRef.current) { + isActionSubmittedRef.current = false + return + } + + event.preventDefault() + + await new Promise((resolve) => setTimeout(resolve, 0)) + await (submitMeta ? api.handleSubmit(submitMeta) : api.handleSubmit()) + + if (api.state.isValid) { + isActionSubmittedRef.current = true + ;(event.target as HTMLFormElement).requestSubmit() + } + } + } return extendedApi }) From 592dd544235feb67db5ab154580a0d958a978a68 Mon Sep 17 00:00:00 2001 From: Kacper Polak Date: Thu, 20 Mar 2025 16:19:43 +0100 Subject: [PATCH 2/3] refactor(react-form): move action-submit to 'react-form/nextjs' --- .../src/app/client-component.tsx | 5 +-- packages/react-form/src/nextjs/index.ts | 1 + .../react-form/src/nextjs/useActionSubmit.ts | 33 +++++++++++++++++++ packages/react-form/src/useForm.tsx | 26 +-------------- 4 files changed, 38 insertions(+), 27 deletions(-) create mode 100644 packages/react-form/src/nextjs/useActionSubmit.ts diff --git a/examples/react/next-server-actions/src/app/client-component.tsx b/examples/react/next-server-actions/src/app/client-component.tsx index 9ac4d21e8..d7fc43675 100644 --- a/examples/react/next-server-actions/src/app/client-component.tsx +++ b/examples/react/next-server-actions/src/app/client-component.tsx @@ -2,7 +2,7 @@ import { useActionState } from 'react' import { mergeForm, useForm, useTransform } from '@tanstack/react-form' -import { initialFormState } from '@tanstack/react-form/nextjs' +import { initialFormState, useActionSubmit } from '@tanstack/react-form/nextjs' import { useStore } from '@tanstack/react-store' import someAction from './action' import { formOpts } from './shared-code' @@ -18,10 +18,11 @@ export const ClientComp = () => { ), }) + const onActionSubmit = useActionSubmit(form) const formErrors = useStore(form.store, (formState) => formState.errors) return ( - + {formErrors.map((error) => (

{error}

))} diff --git a/packages/react-form/src/nextjs/index.ts b/packages/react-form/src/nextjs/index.ts index c839700b1..a8d06d89a 100644 --- a/packages/react-form/src/nextjs/index.ts +++ b/packages/react-form/src/nextjs/index.ts @@ -3,3 +3,4 @@ export * from '@tanstack/form-core' export * from './createServerValidate' export * from './error' export * from './types' +export * from './useActionSubmit' diff --git a/packages/react-form/src/nextjs/useActionSubmit.ts b/packages/react-form/src/nextjs/useActionSubmit.ts new file mode 100644 index 000000000..02c877220 --- /dev/null +++ b/packages/react-form/src/nextjs/useActionSubmit.ts @@ -0,0 +1,33 @@ +import { useCallback, useRef } from 'react' +import type { FormEventHandler } from 'react' +import type { FormApi } from '@tanstack/form-core' + +export const useActionSubmit = ( + form: FormApi, +) => { + const isActionSubmittedRef = useRef(false) + + const onActionSubmit = useCallback( + (submitMeta?: TSubmitMeta): FormEventHandler => { + return async (event) => { + if (isActionSubmittedRef.current) { + isActionSubmittedRef.current = false + return + } + + event.preventDefault() + + await new Promise((resolve) => setTimeout(resolve)) + await (submitMeta ? form.handleSubmit(submitMeta) : form.handleSubmit()) + + if (form.state.isValid) { + isActionSubmittedRef.current = true + ;(event.target as HTMLFormElement).requestSubmit() + } + } + }, + [form], + ) + + return onActionSubmit +} diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 0d2fb8f78..30da1d664 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -11,7 +11,7 @@ import type { FormState, FormValidateOrFn, } from '@tanstack/form-core' -import type { FormEventHandler, PropsWithChildren, ReactNode } from 'react' +import type { PropsWithChildren, ReactNode } from 'react' import type { FieldComponent } from './useField' import type { NoInfer } from '@tanstack/react-store' @@ -80,12 +80,6 @@ export interface ReactFormApi< ) => TSelected children: ((state: NoInfer) => ReactNode) | ReactNode }) => ReactNode - /** - * A function that handles form submissions with server-actions support. Internally calls `handleSubmit` method, but additionally takes care of client-side validation. - */ - handleActionSubmit: ( - submitMeta?: TSubmitMeta, - ) => FormEventHandler } /** @@ -209,24 +203,6 @@ export function useForm< /> ) } - extendedApi.handleActionSubmit = (submitMeta) => { - return async (event) => { - if (isActionSubmittedRef.current) { - isActionSubmittedRef.current = false - return - } - - event.preventDefault() - - await new Promise((resolve) => setTimeout(resolve, 0)) - await (submitMeta ? api.handleSubmit(submitMeta) : api.handleSubmit()) - - if (api.state.isValid) { - isActionSubmittedRef.current = true - ;(event.target as HTMLFormElement).requestSubmit() - } - } - } return extendedApi }) From e0dcbf0f7ec28f2f19398c50a4b59d22a72d46bc Mon Sep 17 00:00:00 2001 From: Kacper Polak Date: Thu, 20 Mar 2025 16:23:29 +0100 Subject: [PATCH 3/3] refactor(react-form): remove unnecessary isActionSubmittedRef from useForm hook --- packages/react-form/src/useForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 30da1d664..2831190d1 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -164,7 +164,6 @@ export function useForm< TSubmitMeta >, ) { - const isActionSubmittedRef = React.useRef(false) const [formApi] = useState(() => { const api = new FormApi< TFormData,