chungweileong94 / server-act

A simple React server action builder
MIT License
25 stars 1 forks source link

Return formErrors in the action builder itself and general questions #29

Open JClackett opened 1 month ago

JClackett commented 1 month ago

Hi! nice work on this!

I've built a very similar library to handle next server actions, but could never get the type-safety exactly right (its pretty close), yours comes closer, you also have middleware which is nice! What im missing from this library though is just the ability to return a union for the form validation errors along with the response from the action, its not super ideal to have to check if formErrors exists in every action and just return early each time. Any ideas how you would modify it to allow that?

Current:

export const loginUser = serverAct
  .input(userSchema.pick({ email: true, password: true }))
  .formAction(async ({ input, formErrors }) => {
    if (formErrors) return { fieldErrors: formErrors.flatten().fieldErrors }
    const user = await db.query.users.findFirst({ where: eq(users.email, input.email) })
    if (!user || !(await bcrypt.compare(input.password, user.password))) return { formError: "Invalid email or password" }
    await Auth.setSession(user.id)
    redirect("/")
  })

Wanted:

export const loginUser = serverAct
  .input(userSchema.pick({ email: true, password: true }))
  .formAction(async ({ input }) => {
    // input is already validated
    const user = await db.query.users.findFirst({ where: eq(users.email, input.email) })
    if (!user || !(await bcrypt.compare(input.password, user.password))) return { formError: "Invalid email or password" }
    await Auth.setSession(user.id)
    redirect("/")
  })

I guess the main issue is the assumption that everyone wants the flattened version, but I also dont mind forking and having my own version.

I've created helper components like Form and FormField that pass the action result into context. FormField, FormError, FormButton can all check their own states and render errors if necessary, so my forms end up just like this:

import { Form, FormField, FormButton, FormError } from "@/components/Form"
import { registerUser } from "../actions"

export default function Page() {
  return (
    <Form action={registerUser} className="space-y-4">
      <h2 className="text-2xl font-bold">Register</h2>
      <FormField label="Name" type="text" name="name" required />
      <FormField label="Email" type="email" name="email" required />
      <FormField label="Password" type="password" name="password" required />
      <FormButton>Register</FormButton>
      <FormError />
    </Form>
  )
}

All automatic handling of errors, loading states, have extended useActionState to have onSuccess and onError callbacks which can be optionally added to the Form component.

Curious to discuss!

chungweileong94 commented 1 month ago

its not super ideal to have to check if formErrors exists in every action and just return early each time. Any ideas how you would modify it to allow that?

That was the original idea, but it wasn't as flexible as I thought. I also not too sure if people are using .flatten or .format, or in other words, I leave most of return parts to the user🙂

chungweileong94 commented 1 month ago

In fact, this is the project I originally inspired from https://www.remix-validated-form.io/, which got almost everything that you described.

chungweileong94 commented 1 month ago

We could probably do something like having another .managedFormAction that will works with a custom form components.