IdoPesok / zsa

https://zsa.vercel.app
MIT License
436 stars 13 forks source link

Feature Request: Ability to validate input on form change or on input change #130

Closed discoverlance-com closed 1 week ago

discoverlance-com commented 2 weeks ago

This is more of a feature request than an issue. I used a hook from the project, kirimase, useValidatedForm, please find this code below:

"use client";

import { FormEvent, useState } from "react";
import { ZodSchema } from "zod";

type EntityZodErrors<T> = Partial<Record<keyof T, string[] | undefined>>;

export function useValidatedForm<Entity>(insertEntityZodSchema: ZodSchema) {
  const [errors, setErrors] = useState<EntityZodErrors<Entity> | null>(null);
  const hasErrors =
    errors !== null &&
    Object.values(errors).some((error) => error !== undefined);

  const handleChange = (event: FormEvent<HTMLFormElement>) => {
    const target = event.target as EventTarget;
    if (
      target instanceof HTMLInputElement ||
      target instanceof HTMLSelectElement ||
      target instanceof HTMLTextAreaElement
    ) {
      if (!(target instanceof HTMLInputElement && target.type === "submit")) {
        const field = target.name as keyof Entity;
        const result = insertEntityZodSchema.safeParse({
          [field]: target.value,
        });
        const fieldError = result.success
          ? undefined
          : result.error.flatten().fieldErrors[field];

        setErrors((prev) => ({
          ...prev,
          [field]: fieldError,
        }));
      }
    }
  };
  return { errors, setErrors, handleChange, hasErrors };
}

It basically receives a zod schema and returns errors and such utilities for interacting with the schema, but with this hook, I can basically add an onChange={handleChange} event to my form which then allows me to get errors as I type in the input. I think this will also be very useful for uses cases where the user is interested in such interactivity to handle errors on the fly as I also do whilst user types into the input.

Since zsa-react-zod functions like useServerAction has access to the schema, could it not also handle something like this and return a handFormChange and a handleInputChange function? In this case, the handleFormChange change could perform similar to this hook and validate each input on change and the handeInputChange could also be applied to individual input like handleInputChange('name') by accepting a field name or name input and return error only for that name input?

Maybe this could also help with this issue: #124 as the user could get the input validated before even submitting so will not get that user experience of the form flicking to show the new errors. Of course this does not mean that the form will not be validated on the server as in my use case with the useValidatedForm hook I shared here, I also use the same zod schema in my server action to validate the schema once more but it takes the pain of having the user wait to submit the form before they get errors and it can also cherry pick each input field so user could type in an input and get errors for that input only without running the validation for the other inputs.

Could this probably work best in the useActionState hook or perhaps a new hook altogether? I have only tried to use zsa-react for a day so I have not been able to deep dive into the package to see if this is already possible but at least from the docs, I don't see such a feature for live updates of errors. I will take the time this week to also browse the codebase to see if I could suggest a solution but I am adding it here just in case anyone has ideas on it or it's already in the works/working. Thanks.

IdoPesok commented 2 weeks ago

Hi, thank you for this detailed request and the code snippet.

Since zsa-react-zod functions like useServerAction has access to the schema

Unfortunately useServerAction actually does not have access to the schema from the action alone. This is because only async functions can be exported from the use server files where the actions are stored. To get access to the schema on the client one can:

  1. Define a shared schema in its own file then import it in the server context and client context.
  2. Send the schema over the network by serializing it then deserializing it on the client.

Option 1 in this case is better because you wouldn't want to wait for the schema to arrive to be able to show the client component.

From my understanding, react-hook-form already does this on change validation with resolvers. I think if zsa-react did have access to the schemas directly from actions -> it would make sense to include a function like this in the hook. However, since it doesn't have access to the schema, it pretty much turns into the exact same thing as react hook form resolvers where you need to pass your own schema.

If you find a way around this limitation, happy to review.

discoverlance-com commented 2 weeks ago

I see so it looks like it will not be possible ideally then... mostly given that the action and schema created is basically in a server action.

I think the best bet might be that given a createSeverAction from zsa, we can extract out the input object and then pass it into another custom hook like the useValidated form above:

// action.ts
"use server"

import { createServerAction } from "zsa"
import { incrementSchema } from '@/lib/zod/schema'
import z from "zod"

export const incrementNumberAction = createServerAction() 
    .input(incrementSchema)
    .handler(async ({ input }) => {
        // Sleep for .5 seconds
        await new Promise((resolve) => setTimeout(resolve, 500))
        // Increment the input number by 1
        return input.number + 1;
    });

// lib/zod/schema.ts
import { z } from 'zod';
export const incrementSchema = z.object({
        number: z.number()
    });

// increment.tsx
const [data, err] = await incrementNumberAction({ number: 24 });  // or  
const { isPending, execute, data, error, isError } = useServerAction(incrementNumberAction);
// and then for the client one
const { errors, setErrors, handleChange, hasErrors } = useValidatedForm(incrementSchema);

The problem now is that the errors can now be accessed from two different places even though it's the same zod schema so it will now be difficult to know which error to use or go for as you have two source of truths which is not the best and you will have to check the errors from two places.

But will this also not work the same way with the useServerAction hook if we still put our zod schema in a different file and then, we can import it into our server action and also the useServerAction hook and add that functionality? Or create another hook that does a similar thing to useServerAction but allows you to pass in a zod schema to get client side error handling?


// action.tsx
"use server"

import { createServerAction } from "zsa"
import { incrementSchema } from '@/lib/zod/schema'
import z from "zod"

export const incrementNumberAction = createServerAction() 
    .input(incrementSchema)
    .handler(async ({ input }) => {
        // Sleep for .5 seconds
        await new Promise((resolve) => setTimeout(resolve, 500))
        // Increment the input number by 1
        return input.number + 1;
    });

// lib/zod/schema.ts
import { z } from 'zod';
export const incrementSchema = z.object({
        number: z.number()
    });

// increment.tsx
const { isPending, execute, data, error, isError, handleInputChange, handleFormChange } 
= useServerAction(incrementNumberAction, incrementSchema);
// or another hook that accepts a schema as a second parameter and we keep useServerAction as is
const { isPending, execute, data, error, isError, handleInputChange, handleFormChange } 
= useClientAction(incrementNumberAction, incrementSchema);
IdoPesok commented 2 weeks ago

Maybe an optional second arg to useServerAction and if that arg is passed in then it will return the extra schema hooks?