Published on

How I Will Be Doing Forms (React Hook Form + shadcn/ui)

Shoutout To The Sources

Some of the things my current passion project emphasizes are going directly to the source, and sharing your sources. This method would have taken me much longer to figure out without this excellent blog post.

This video and their code example also helped a bunch

Let's Dive In

I was really set on using React's new useActionState hook previously (previously useFormState). You pass it a form action function and an initial state, it returns a new action that you use in your form and the latest form state. It also provides an isPending boolean, if you'd like to show the user some sign the submission is pending!

Schema, FormState Type and Initial useActionState() Data

const AddSourceSchema = z.object({
  title: z.string().min(2, 'Invalid'),
  url: z.string().min(2, 'Invalid'),
  scrape: z.boolean(),
  count: z.string().min(1, 'Invalid').optional(),
})

type FormState = {
  message: string
  fields?: Record<string, string>
  issues?: string[]
}

Use in Server Action

export async function addSourceAction(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const formObj = Object.fromEntries(formData.entries())
  const { success, data } = AddSourceSchema.safeParse(formObj)
  // Perform data operations...
}

Form After Setup With React Hook Form

export function AddSourceComponent2() {
  const [state, formAction] = useActionState(addSource, {
    message: '',
  })
  const form = useForm<z.output<typeof AddSourceSchema>>({
    resolver: zodResolver(AddSourceSchema),
    defaultValues: {
      title: '',
      url: '',
      scrape: false,
      ...(state?.fields ?? {}),
    },
  })
  const scrapeValue = form.watch('scrape')
  const formRef = useRef<HTMLFormElement>(null)
  return (
    <Card className="m-10">
      <Form {...form}>
        {state?.message !== '' && !state.issues && (
          <div className="text-red-500">{state.message}</div>
        )}
        {state?.issues && (
          <div className="text-red-500">
            <ul>
              {state.issues.map((issue) => (
                <li key={issue} className="flex gap-1">
                  <X fill="red" />
                  {issue}
                </li>
              ))}
            </ul>
          </div>
        )}
        <form
          ref={formRef}
          className="space-y-8"
          action={formAction}
          onSubmit={(evt) => {
            evt.preventDefault()
            form.handleSubmit(() => {
              formAction(new FormData(formRef.current!))
            })(evt)
          }}
        >
          <div className="flex gap-2">
            <FormField
              control={form.control}
              name="title"
              render={({ field }) => (
                <FormItem className="w-full">
                  <FormLabel>Title</FormLabel>
                  <FormControl>
                    <Input placeholder="" {...field} />
                  </FormControl>
                  <FormDescription>Title for new RSS feed source</FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />
            <FormField
              control={form.control}
              name="url"
              render={({ field }) => (
                <FormItem className="w-full">
                  <FormLabel>URL</FormLabel>
                  <FormControl>
                    <Input placeholder="" {...field} />
                  </FormControl>
                  <FormDescription>URL of new RSS feed source</FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />
          </div>
          <FormField
            control={form.control}
            name="scrape"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Start Scraping Job For New Source</FormLabel>
                <FormControl>
                  <Switch checked={field.value} onCheckedChange={field.onChange} />
                </FormControl>
                <FormDescription>Enable if wishing to start scraping immediately</FormDescription>
                <FormMessage />
              </FormItem>
            )}
          />

          {scrapeValue && (
            <FormField
              control={form.control}
              name="count"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Minutes Between Scrapes</FormLabel>
                  <FormControl>
                    <Input placeholder="" {...field} />
                  </FormControl>
                  <FormDescription>Enter the time between scraping job runs</FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />
          )}
          <Button type="submit">Submit</Button>
        </form>
      </Form>
    </Card>
  )
}

This allows displaying errors targeted at a specific form field! That is on top of the validation we get from zod. This is my current go to method! Feel free to reach out if you have a better method!