IdoPesok / zsa

https://zsa.vercel.app
MIT License
415 stars 12 forks source link

`isPending` not getting updated #63

Closed stefanosandes closed 4 weeks ago

stefanosandes commented 1 month ago

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

stefanosandes commented 1 month ago

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?

IdoPesok commented 1 month ago

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.

IdoPesok commented 1 month ago

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.

IdoPesok commented 1 month ago

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>
  );
};
stefanosandes commented 1 month ago

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.

IdoPesok commented 1 month ago

Have you played around with react hook form? It provides a way to reset specific fields.

1) 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>
  );
}
stefanosandes commented 1 month ago

@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.

IdoPesok commented 4 weeks ago

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.

stefanosandes commented 4 weeks ago

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.

IdoPesok commented 3 weeks ago

Hi, wanted to check back in and report an update in the latest version of zsa-react. You can now use useServerAction alongside <form action={...} /> using the new function executeFormAction

Here are the docs