`isPending` not getting updated
stefanosandes opened this issue · comments
Hello guys, very cool lib!
I don't know if I'm doing something wrong, but I can't get isPending
updated and status
never gets to pending
.
Am I missing something?
Reproduction: https://codesandbox.io/p/devbox/zsa-ispending-problem-rhdvmm
Ok, I tried to call the execute
from the button's onClick
and it worked. I think there's no way to submit from a form action
and get the benefits of useServerAction
? The docs show how to use it with useFormState
, but then we can't get the cool things from useServerAction
like error states and pending state, right?
Hi -- thank you and happy to help!
Was just about to say there would be an issue using form's action and useServerAction
together. If you want the pending state, useFormState
has been updated to useActionState
in newer versions of react and has isPending
as the third index in the returned tuple.
One sec, will write up an example here of how to make it work with useActionState
.
Okay, here I got it working with isPending, data, and error states. This is on Next JS RC which as useActionState
.
Step 1 is to make sure you have { type : "formData" }
on your input.
actions.ts
"use server";
import { z } from "zod";
import { createServerAction } from "zsa";
export const zsaAction = createServerAction()
.input(
z.object({
name: z.string().min(4),
}),
{
type: "formData",
}
)
.handler(async ({ input }) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log(input);
});
Now we are ready to use useActionState
. Note, you will not need to do formData.get
since this is done by zsa
when you set the input type to be formData.
"use client";
import { zsaAction } from "./action";
import { useActionState } from "react";
import { inferServerActionReturnType } from "zsa";
export const ZSAForm = () => {
const [state, submit, isPending] = useActionState(
async (
prevState: inferServerActionReturnType<typeof zsaAction> | null,
formData: FormData
) => {
return await zsaAction(formData);
},
null
);
return (
<form action={submit}>
<input type="text" name="name" />
<button type="submit">Submit</button>
<pre>{JSON.stringify({ isPending }, null, 2)}</pre>
{state && state[1] && (
<pre>{JSON.stringify(state[1].fieldErrors?.name)}</pre>
)}
</form>
);
};
The rundown here is that state
starts as null (idle state) and then will get updated to either be [data, null] or [null, err] once the result comes in. You will have access to typed data and error once you check that state is not null. state[0] is data and state[1] is the error.
Lmk if this works for you.
Cleaned the client up a bit here:
"use client";
import { zsaAction } from "./action";
import { useActionState } from "react";
import { inferServerActionReturnType } from "zsa";
export const ZSAForm = () => {
const [[data, err], submit, isPending] = useActionState(
async (prevState: any, formData: FormData) => await zsaAction(formData),
[null, null] as inferServerActionReturnType<typeof zsaAction> | [null, null]
);
return (
<form action={submit}>
<input type="text" name="name" />
<button type="submit">Submit</button>
<pre>{JSON.stringify({ isPending }, null, 2)}</pre>
{err && <pre>{JSON.stringify(err.fieldErrors?.name)}</pre>}
</form>
);
};
Hey @IdoPesok, very appreciate your attention here man!
The last solution is simpler and less verbose.
For my use case, I think I'll need to use useTransition
to get the pending state while using useServerAction
. The reset
method is very useful for me to reset the error on fields when users start typing.
It could be even better if I could reset specific fields to keep things consistent, like reset('name')
or something like that.
Anyway, if you're not planning to implement a way to use useServerAction
without the need to useActionState
, I think that it will be helpful to include this example in docs.
This lib has a great potential, man. Congratulations on the ideia and implementation, and thanks, it will help me a lot.
Have you played around with react hook form? It provides a way to reset specific fields.
- Without react hook form:
"use client";
import { zsaAction } from "./action";
import { useServerAction } from "zsa-react";
export const ZSAForm = () => {
const { isPending, execute, isSuccess, data, isError, error } =
useServerAction(zsaAction);
return (
<form
onSubmit={async (event) => {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
const [data, err] = await execute(formData);
if (err) return;
form.reset();
}}
>
<input type="text" name="name" style={{ color: "black" }} />
<button type="submit">Submit</button>
{isPending && <div>Loading...</div>}
{isSuccess && <div>Success: {JSON.stringify(data)}</div>}
{isError && <div>Error: {JSON.stringify(error.fieldErrors)}</div>}
</form>
);
};
With react hook form
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useServerAction } from "zsa-react";
import { zsaAction } from "./action";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
const formSchema = z.object({
name: z.string().min(2, {
message: "Name must be at least 2 characters.",
}),
});
export function ProfileForm() {
const { isPending, execute } = useServerAction(zsaAction);
// 1. Define your form.
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
},
});
// 2. Define a submit handler.
async function onSubmit(values: z.infer<typeof formSchema>) {
const [data, err] = await execute(values);
if (err) {
// show a toast or something
return;
}
form.reset({ name: "" });
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button display={isPending} type="submit">
{isPending ? "Saving..." : "Save"}
</Button>
</form>
</Form>
);
}
@IdoPesok Yes, I have used RHF in some projects. It can be a bit verbose unless you use with some UI lib that integrates with it, like shadcnui. And I think they don't do much to best integrate RHF with server actions. If you want a double check validating data on the server, for example, you need more boilerplate and code duplication. But mixing RHF with ZSA may be the best solution for more advanced cases.
Hi, in zsa@0.3.0
forms got a lot better. Please check out our new docs. Will close this issue for now, thank you for helping us improve this.
Hey @IdoPesok -- thank you for your effort to improve it!
One last thing, it's may be possible to export the schema
from useServerAction
? Something like const { inputSchema } = useServerAction(produceNewMessage)
. It will be useful in the react-hook-forms example, because no schema duplication will be needed. The schema could be exported from another file and used on both sides as well, but I think it could be a good addition. The final code could be like this:
const { isPending, execute, data, error, inputSchema } = useServerAction(produceNewMessage)
const form = useForm<z.infer<typeof inputSchema>>({
resolver: zodResolver(inputSchema),
defaultValues: {
name: "",
},
})
With a small utility function (it could be done by each developer, or as an integration package in the future), it could apply ZSA errors to RHF form easily, something like setHRFErrors(form, error)
where form
is RHF instance and error
the ZSA validation errors.
Just some aleatory ideas based on my needs, but may be can be useful.
And again, thanks for your time on this job, man. It's helping a lot! Really great work.