New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support Server Actions on onSubmit (Next 13) #10391
Comments
My understanding is, you can already utilize the |
I will give it try later with our new Form component, see if we can make it easier to integrate. https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#custom-invocation-using-starttransition |
Just had a quick play, I don't think it's possible, server action is based on the server component which hook form requires client side of state from react. |
Would it be possible to add some sort of react hook form RSC code so we get the validation and a subset of react hook form on the server? Or do I understand it completely wrong? |
Maybe this is a possible solution? We disable the progressive enhancement, however the action runs on the server and we get input validation in the client and can run server code in // app/test/form.tsx
"use client";
import { useTransition } from "react";
import { useForm } from "react-hook-form";
import { action, type FormData } from "./action";
export function Form() {
const [isPending, startTransition] = useTransition();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>();
const onSubmit = handleSubmit(data => {
startTransition(() => {
action(data);
});
});
return (
<form onSubmit={onSubmit}>
<label htmlFor="name">Name</label>
<input type="text" id="name" {...register("name", { required: true })} />
{errors.name && <span>This field is required</span>}
<label htmlFor="description">Description</label>
<input type="text" id="description" {...register("description", { required: true })} />
{errors.description && <span>This field is required</span>}
<button type="submit" disabled={isPending}>
Submit
</button>
</form>
);
} // app/test/action.ts
"use server";
export type FormData = {
name: string;
description: string;
};
export async function action(data: FormData) {
console.log(data);
} // app/test/page.tsx
import { Form } from "./form";
export default function Page() {
return <Form />;
} |
I guess the new Form component simplify a bit return (
<Form onSubmit={({ data }) => {
startTransition(() => {
action(data);
});
}}>
<input {...register('input')} />
</Form>
); without server action return (
<Form action="/api" method="post">
<input {...register('input')} />
</Form>
); |
@bluebill1049
Here is a poor POC: So it would be great if RHF could support this behavior with |
cc @kotarella1110 above |
I don't think you need to remove You can pass server actions directly to your client something like function CreateItemForm() {
const { handleSubmit } = useForm()
async function addItem(data) {
'use server'
return await db.add('items', data)
}
const onSubmit = (data) => addItem(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
...
</form>
)
} |
To support progressive enhancement, it might be a good suggestion to avoid calling |
hmmm, not sure if I am following this one. If js is disabled then do we even need to worry about |
I'm not sure if the concept of progressive enhancement is currently worth pursuing for a form management library, TBH. It's a more nuanced topic, but from my POV, the most apparent concern regarding RSC and Server Actions is the lack of a comprehensive solution for error handling and tracking additional form states. You still want to have a client form component that can maintain form state like submission status and validity. Additionally, it should be capable of displaying form validation on the page, which, I believe, isn't a thing with the new server stuff ATM. |
^ a quick note, I'm not trying to sway the conversation away from exploring progressive enhancement; just sharing my perspective on the current state of things with the new server stuff |
if JS is disabled, there is no need to worry about it. However, if JS is enabled, calling function Form() {
return (
<form
// When JS is enabled, server actions will not be executed.
action={serverAction}
onSubmit={(event) => {
event.preventDefault();
}
}>
<input type="text" name="text" defaultValue="" />
<button>Submit</button>
</form>
);
} |
Got it @kotarella1110 personally I do agree with what @Moshyfawn stated above, this library focuses on providing great user experience while leveraging a lot of client-side validation and state management. Progressive enhancement is a great bounce tho. |
related links: |
Error handling part almost felt the current Form design is more generic and not vendor locking to any framework // Send post request with json form data
<Form action="/api" encType="application/json" headers={{ accessToken: 'test' }}>
{errors.root?.server.type === 500 && 'Error message'}
{errors.root?.server.type === 400 && 'Error message'}
</Form> |
Ooo, interesting! A hook to track action states.. Granted, it's still hypothetical, it ties into a couple of ideas I had about the API. Lemme sit on that 🤔 |
What's the point of |
|
I read this section of the docs like 10 times already and even tried it out myself. Not wrapping the server action into a startTransition changed nothing. That's why I'm wondering. |
@codinginflow (cc @Moshyfawn) I'm also a bit confused by the docs. I think if you do a mutation (e.g. add an element to a list) and then execute I tried putting a sleep in the function that renders the new data (to make the waiting time noticeable), however the weird thing is that only sometimes it waits for the new data. Sometimes the (Note: I'm not sure about anything I wrote... I wish docs were more detailed) |
Yea, I noticed the same. This doesn't work in like 5-10% of cases for me tho. It's probably a bug. In earlier versions of Next, the |
I'm using the <form
action={async () => {
const valid = await form.trigger();
if (valid) createApplePass({ data: form.getValues() });
}}
className="space-y-8"
>
</form> |
I've created an experimental PR to explore the best way to support Server Actions within React Hook Form. Your thoughts and ideas are invaluable, so please provide feedback! |
Just for anyone stumbling across this, I was able to get it to work (requires js) <form
action={async (formData: FormData) => {
const valid = await form.trigger();
if (!valid) return;
return serverAction(formData);
}}
> |
Here is the way I've been using server actions in react-hook-forms: export async function register(newUser: unknown) {
// Server-side validation
const newUserValidation = UserSchema.safeParse(newUser);
if (!newUserValidation.success) {
// ...handle error messages
}
// ...add new user to database
} export function NewUserForm() {
const clientAction = async(formData: FormData) => {
// Create new user object from form data with .get()
const newUser = {
email: formData.get("email"),
password: formData.get("password")
};
// Add client-side validation here
const result = UserSchema.safeParse(newUser);
if (!result.success) {
let errorMessage = "";
result.error.issues.forEach((issue) => {
errorMessage += issue.path[0] + " " + issue.message + ". ";
});
console.log(errorMessage);
return;
}
// Handle server action and server-side errors
const response = await register(result.data);
if(response?.error) {
console.log(response.error);
return;
}
}
return (
<form action={clientAction}>...
)
} This way you get client and server-side validation and can handle error messages for both in the client and you avoid having to use trigger or useTranslation, etc. |
I was able to do what I wanted with this. const NO_OPERATION = () => {};
<form
{...(formState.isValid ? { action: serverAction } : { onSubmit: handleSubmit(NO_OPERATION) })}
> With this, If the form becomes valid, you could use serverAction. |
It works correctly (async server actions in on submit of react-hook-form): 'use client'
import { useForm, SubmitHandler } from "react-hook-form"
import { yourServerAction } from "dir-to-your-server-action"
type Inputs = {
name: string
}
export default function ExampleForm() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm<Inputs>()
const onSubmit: SubmitHandler<Inputs> = async (data) => {
await yourServerAction(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)} method="POST">
<input
id="name"
{...register('name', { required: true })}
type="text"
/>
<button type="submit">Submit</button>
</form>
)
} |
Yes that's working, but then we lost Progressive Enhancement. Have you found a way to do shadcnUI form with client-side validation and actions |
One caveat to note: If you're using |
i created this custom hook (based on the answers above), i don't know if there are any implications on this, but this lets me do the same thing as i would do in the onSubmit. This will also make it so the type of the formData is your schema, instead of just FormData import { FieldValues, SubmitHandler, UseFormProps, UseFormReturn, useForm } from "react-hook-form";
export function useFormAction<TFieldValues extends FieldValues = FieldValues, TContext = any, TTransformedValues extends FieldValues | undefined = undefined>(props?: UseFormProps<TFieldValues, TContext>){
const form = useForm<TFieldValues, TContext, TTransformedValues>(props)
const handleAction = async (onAction: SubmitHandler<TFieldValues>) => {
const valid = await form.trigger();
if (valid) {
return onAction(form.getValues());
}
};
return {
...form,
handleAction
}
} To use, just replace your useForm with this useFormAction, ideally it should work, please let me know if there are any issues with this "use client";
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
const formSchema = z.object({
email: z.string().email(),
})
export type EmailCheckFormSchema = z.infer<typeof formSchema>
export function Example() {
const form = useFormAction<EmailCheckFormSchema>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
},
})
return (
<form action={() => form.handleAction(emailCheck)}>
</form>
)
} "use server";
export async function emailCheck(formData: EmailCheckFormSchema)
{
//do something
} |
I think there is a way to keep progressive enhancement working, and it's quite simple. The idea is to use both 'use client';
import { useFormState } from 'react-dom';
import { useForm } from 'react-hook-form';
import { authenticate } from '../server-actions';
export function AuthForm() {
const [state, action] = useFormState(authenticate, undefined);
const form = useForm({
...,
errors: state.errors, // use errors from the server when submitted without JS
});
return (
<form
action={action} // from React's "useFormState()"
onSubmit={(event) => {
const formData = new FormData(
event.target as HTMLFormElement,
(event.nativeEvent as SubmitEvent).submitter,
);
form.handleSubmit(async () => {
// execute the server action and sync errors to the form
const { errors } = await authenticate(state, formData);
if (errors) {
form.setError(...);
}
})(event);
}}
>
...
</form>
);
} It's not perfect, as React's useFormStatus will not work with the |
Hi @ezanglo Here's my code: 'use client';
import { Button } from '@/components/ui/button'
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
const formSchema = z.object({
displayName: z.string().min(2).max(50),
// ...
})
export type MyFormSchema = z.infer<typeof formSchema>
export function Example() {
const form = useFormAction<EmailCheckFormSchema>({
resolver: zodResolver(formSchema),
defaultValues: {
displayName: '',
// ...
},
mode: 'onChange',
criteriaMode: 'all',
shouldFocusError: false,
})
return (
<form action={() => form.handleAction(submitMyForm)}>
// ...
<Button className='w-full' type='submit' disabled={form.formState.isSubmitting}>
Submit
</Button>
</form>
)
} 'use server';
export async function submitMyForm(formData: MyFormSchema) {
await new Promise(resolve => setTimeout(resolve, 10000))
console.log('done')
} When I click the submit button, Any help would be appreciated! |
@gendaineko2222 since we are using form action, isSubmitting will not work coz i think it only triggers when you pass something to onSubmit. You can however, use "pending" from the useFormState, check the implementation here |
Thanks for your help! I was able to get it working by using the I was originally trying to use the Here is the documentation that helped me: |
@gendaineko2222 @ezanglo |
@robahtou thanks for this, i think you can use the schema to parse the result, and get the transformed value. I have also created a repo to experiment on some of the solutions on this thread. here is the updated useFormAction import { zodResolver } from "@hookform/resolvers/zod";
import { FieldValues, SubmitHandler, useForm, UseFormProps } from "react-hook-form";
import { z } from "zod";
type UseFormActionProps<TFieldValues extends FieldValues = FieldValues, TContext = any> = UseFormProps<TFieldValues, TContext> & {
schema: z.Schema<any, any>
}
export function useFormAction<TFieldValues extends FieldValues = FieldValues, TContext = any, TTransformedValues extends FieldValues = TFieldValues>({
schema,
...props
}: UseFormActionProps<TFieldValues, TContext>) {
const form = useForm({
...props,
resolver: zodResolver(schema)
})
const handleAction = async (onAction: SubmitHandler<TFieldValues>) => {
const valid = await form.trigger();
if (valid) {
return onAction(schema.parse(form.getValues()));
}
};
return {
...form,
handleAction
}
} and to use it, just include the schema const form = useFormAction<UseFormActionFormSchema>({
schema: formSchema,
resolver: zodResolver(formSchema),
defaultValues: {
email: 'email@example.com',
},
}) |
@ezanglo thanks for the tip. But now it looks like you have to validate twice, once indirectly with RHF ( If you are going to use const handleAction = async (onAction: SubmitHandler<TFieldValues>) => {
const valid = schema.safeParse(form.getValues());
if (valid.success) {
return onAction(valid.data);
}
// handle errors `valid.error`
}; and if you continue with this line of thought, in specific use cases regarding server actions, should RHF be used for validation or strictly use zod? Use RHF for other form features but validate explicityly with zod? |
@robahtou yes, that would be the case, I think the fix should really be from RHF, i saw that even the watch doesn't provide the transformed values, we need a way to get the value from the resolver. const form = useFormAction<UseFormActionFormSchema>({
schema: formSchema,
getTransformedValues: (values) => {
const validatedFields = formSchema.safeParse(values)
return validatedFields.success ? validatedFields.data : values
},
resolver: zodResolver(formSchema),
defaultValues: {
email: 'email@example.com',
},
}) I agree with the double validation, the problem with manually doing the safeParse is it doesn't actually trigger the errors, so you need to manually set the errors into the form, I am experimenting on that too, you can check the commented code in here |
@ezanglo Also, as other have already commented, The ideal case would be the RHF adopts necessary changes to support server actions (progressive enhancement, etc) and out of the box form state (instead of explicitly coding In other words, use |
I found a solution that does client-side verification while maintaining progressive enhancement. <form
ref={formRef}
action={formAction}
onSubmit={e => {
trigger()
if (formState.isValid) {
formRef.current?.requestSubmit()
} else {
e.preventDefault()
}
}}
> |
hey @hunterbecton. I'm using safari browser with javascript disabled. WIth your example nothing happens when I submit the form. Same on Firefox. Can you share a working code sample? |
Hey @robahtou I tested it on Safari, and everything worked as expected ( I did not test on Firefox ). My code is below. LoginForm.tsx'use client'
import { FC, useEffect, useTransition, useRef } from 'react'
import { useForm, FormProvider } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { LoginFormDataSchema } from '@/lib/validation/auth'
import { useFormState, useFormStatus } from 'react-dom'
import { loginPasswordless, loginGitHub } from '@/lib/actions/authActions'
import { Button } from '@/components/button/Button'
import { toast } from 'react-toastify'
import { TextInput } from '@/components/form/TextInput'
import { GitHubIcon } from '@/components/icon/default/frameless/GitHubIcon'
type LoginFormInputs = z.output<typeof LoginFormDataSchema>
export const LoginForm: FC = () => {
const formRef = useRef<HTMLFormElement>(null)
const [state, formAction] = useFormState(loginPasswordless, null)
const methods = useForm<LoginFormInputs>({
resolver: zodResolver(LoginFormDataSchema),
mode: 'onBlur',
defaultValues: {
email: '',
...(state?.fields ?? {}),
},
})
const { trigger, reset, formState } = methods
const [isPending, startTransition] = useTransition()
const onLoginGitHub = () => {
startTransition(async () => {
const { status, message } = await loginGitHub()
if (status === 'error') {
toast.error(message)
}
})
}
useEffect(() => {
if (!state) return
if (state.status === 'error') {
toast.error(state.message)
}
if (state.status === 'success') {
toast.success(state.message)
reset()
}
}, [state, reset])
const SubmitButton = () => {
const { pending } = useFormStatus()
return (
<Button
disabled={pending}
type="submit"
text={pending ? 'Processing...' : 'Continue'}
className="mt-4 w-full"
variant="color"
/>
)
}
return (
<>
<FormProvider {...methods}>
<form
ref={formRef}
action={formAction}
onSubmit={e => {
trigger()
if (formState.isValid) {
formRef.current?.requestSubmit()
} else {
e.preventDefault()
}
}}
className="mt-8"
>
<div>
<TextInput
className="w-full"
label="Email"
name="email"
placeholder="Enter your email"
/>
</div>
<SubmitButton />
{state?.status === 'success' && (
<p className="mt-2 text-xs font-normal text-emerald-600">
{state.message}
</p>
)}
{state?.status === 'error' && (
<p className="mt-2 text-xs font-normal text-rose-600">
{state.message}
</p>
)}
<div className="mt-8 flex items-center gap-2">
<div className="h-[1.5px] flex-1 bg-white/10"></div>
<p className="text-sm font-normal text-white">Or continue with</p>
<div className="h-[1.5px] flex-1 bg-white/10"></div>
</div>
<Button
disabled={isPending}
type="button"
text="GitHub"
className="mt-8 w-full"
onClick={onLoginGitHub}
withIcon={true}
icon={<GitHubIcon className="h-[1.125rem] w-auto" />}
/>
</form>
</FormProvider>
</>
)
} authActions.ts'use server'
import { getErrorMessage } from '@/lib/utils/errorHandler'
import { LoginFormDataSchema } from '@/lib/validation/auth'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { formatEmailToUsername } from '@/lib/utils/formatEmailToUsername'
import { createClient } from '@/lib/supabase/server'
export const loginPasswordless = async (prevState: any, data: FormData) => {
const formData = Object.fromEntries(data)
const fields: Record<string, string> = {}
for (const key of Object.keys(formData)) {
fields[key] = formData[key].toString()
}
const parsed = LoginFormDataSchema.safeParse(formData)
if (!parsed.success) {
return {
fieldErrors: parsed.error.flatten().fieldErrors,
fields,
}
}
const supabase = createClient()
try {
const { error } = await supabase.auth.signInWithOtp({
email: parsed.data.email,
options: {
emailRedirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
data: {
user_name: formatEmailToUsername(parsed.data.email),
avatar_url:
'https://axcfyibnfbkmqbvnrcoa.supabase.co/storage/v1/object/public/avatars/default-avatar.png',
},
},
})
if (error) {
throw new Error(error.message)
}
return {
status: 'success',
message: 'Success! A login link was sent to your email!',
}
} catch (error) {
return {
status: 'error',
message: getErrorMessage(error),
fields,
}
}
} Note that it only works when you call |
@hunterbecton thanks for sharing. When you test on Safari make sure when you disable JS, then quit the browser and start it up again. I tested this again in safari and the |
Yeah, startTransition works perfect. I've done with this solution. "use client";
import {
CalculatorFormSchema,
calculatorSchema,
} from "@/components/ui/CaluclatorPage/CalculatorSchema";
import { SubmitButton } from "@/components/ui/FormElements/submitButton";
import { zodResolver } from "@hookform/resolvers/zod";
import Container from "@mui/material/Container";
import Stack from "@mui/material/Stack";
import { useEffect, useRef, useTransition } from "react";
import { useFormState } from "react-dom";
import {
FormProvider,
RadioButtonGroup,
TextFieldElement,
useForm,
} from "react-hook-form-mui";
interface CalculatorFormProps {
onFormAction: (
prevState: {
message: string;
calculator?: CalculatorFormSchema;
issues?: string[];
},
data: FormData,
) => Promise<{
message: string;
calculator?: CalculatorFormSchema;
issues?: string[];
status?: number;
}>;
}
export const CalculatorForm = ({ onFormAction }: CalculatorFormProps) => {
const formRef = useRef<HTMLFormElement>(null);
const [state, formAction] = useFormState(onFormAction, { message: "test" });
const [isPending, startTransition] = useTransition();
const form = useForm<CalculatorFormSchema>({
resolver: zodResolver(calculatorSchema),
defaultValues: {
investitionDescription: "",
},
});
useEffect(() => {
if (state.status === 200) {
form.reset();
}
}, [state, form]);
return (
<Container maxWidth="sm">
<FormProvider {...form}>
<form
action={formAction}
ref={formRef}
onSubmit={(evt) => {
evt.preventDefault();
startTransition(() => {
form.handleSubmit(() => {
formAction(new FormData(formRef.current!));
})(evt);
});
}}
>
<div>{state?.message}</div>
<Stack alignItems="start">
<TextFieldElement
name="investitionDescription"
color="primary"
label="opis inwestycji"
/>
<RadioButtonGroup
label="Typ inwestycji"
name="investitionType"
options={[
{
id: "droga",
value: "droga",
label: "Droga",
},
{
id: "lotnisko",
value: "lotnisko",
label: "Lotnisko",
},
]}
/>
<SubmitButton isPending={isPending} />
</Stack>
</form>
</FormProvider>
</Container>
);
}; |
I created custom hooks to handle server actions with React Hook Form and Shadcn-ui
Github Repo: https://github.com/kdh379/react-hook-form-server-actions useFormAction "use client";
import { useCallback, useEffect } from "react";
import { FieldValues, useForm, UseFormProps } from "react-hook-form";
import { toast } from "@/components/ui/use-toast";
type UseFormActionProps<TFieldValues extends FieldValues = FieldValues, TContext = any> = UseFormProps<TFieldValues, TContext> & {
state: ActionState | unknown;
onSuccess?: () => void;
}
export function useFormAction<TFieldValues extends FieldValues = FieldValues, TContext = any>({
state,
onSuccess,
...props
}: UseFormActionProps<TFieldValues, TContext>) {
const form = useForm({
...props,
});
const handleSuccess = useCallback(() => {
onSuccess?.();
// eslint-disable-next-line
}, []);
useEffect(() => {
if( !hasState(state) ) return;
form.clearErrors();
switch (state.code) {
case "INTERNAL_ERROR":
toast({
title: "Something went wrong."
description: "Please try again later.",
variant: "destructive",
duration: 5000,
});
break;
case "VALIDATION_ERROR":
const { fieldErrors } = state;
Object.keys(fieldErrors).forEach((key) => {
form.setError(key as any, { message: fieldErrors[key].flat().join(" ") });
});
break;
case "EXISTS_ERROR":
form.setError(state.key as any, { message: state.message });
break;
case "SUCCESS":
toast({
title: state.message,
duration: 5000,
});
handleSuccess();
form.reset();
break;
}
}, [state, form, handleSuccess]);
return {
...form,
};
}
const hasState = (state: ActionState | unknown): state is ActionState => {
if(!state || typeof state !== "object") return false;
return "code" in state;
}; action.d.ts
client side form "use client";
import { useFormState, useFormStatus } from "react-dom";
import { useFormContext } from "react-hook-form";
import { type FormValues, submitForm } from "@/app/actions";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { toast } from "@/components/ui/use-toast";
import { useFormAction } from "@/hooks/useFormAction";
export default function ClientSideForm() {
const [state, formAction] = useFormState(submitForm, null);
const form = useFormAction<FormValues>({
state,
defaultValues: {
email: "",
password: "",
},
onSuccess: () => {
toast({
title: "Form submitted successfully!",
duration: 5000,
});
},
});
return (
<Form {...form}>
<form
action={formAction}
>
<FormFields />
</form>
</Form>
);
}
function FormFields() {
const form = useFormContext();
const { pending } = useFormStatus();
return (
<div className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center justify-between">
Email
<FormMessage />
</FormLabel>
<FormControl>
<Input
{...field}
type="email"
placeholder="abc@abc.com"
disabled={pending}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center justify-between">
Password
<FormMessage />
</FormLabel>
<FormControl>
<Input
{...field}
type="password"
placeholder="********"
disabled={pending}
/>
</FormControl>
</FormItem>
)}
/>
<div className="flex justify-end">
<Button
isLoading={pending}
>
Submit
</Button>
</div>
</div>
);
} server actions "use server";
import { z } from "zod";
const formSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
const EXISTS_USER = [
"abc@abc.com",
];
export type FormValues = z.infer<typeof formSchema>;
export async function submitForm(
_prevState: any,
formData: FormData
): Promise<ActionState | void> {
console.log("server action!!");
// Delay for 1 second
await new Promise((resolve) => setTimeout(resolve, 1000));
const input = formSchema.safeParse({
email: formData.get("email"),
password: formData.get("password"),
});
if (!input.success) {
const { fieldErrors } = input.error.flatten();
return {
code: "VALIDATION_ERROR",
fieldErrors,
};
}
try {
if( EXISTS_USER.includes(input.data.email) ) {
return {
code: "EXISTS_ERROR",
key: "email",
message: "User already exists with this email.",
};
}
// object equality check
return {
code: "SUCCESS",
message: "Form submitted successfully!",
};
}
catch (error) {
return {
code: "INTERNAL_ERROR",
err: error,
};
}
} _20240509_175116.webm |
The works fine. The <form
ref={formRef}
action={formAction}
onSubmit={async (e) => {
await trigger();
if (formState.isValid) {
formRef.current?.requestSubmit();
} else {
console.log(`[SurveyForm] form is invalid`, formState.errors);
e.preventDefault();
}
}}
> |
Do you have solution for triggering toast in this code? |
@tuon1602 you can try to put the toast after the transition |
Next Js 13 Server actions are in alpha stages but any chance we can get this going?
For example instead of passing onSubmit with the handleSubmit, perhaps something to pass to formActions instead?
The text was updated successfully, but these errors were encountered: