🥴

conformとserver actionの汎用のhook

2024/08/29に公開

概要

conformとserver actionの汎用のhookを実装するだけ

参考

実装

//
import type { FormOptions, SubmissionResult } from "@conform-to/dom"
import { useForm } from "@conform-to/react"
import { getZodConstraint, parseWithZod } from "@conform-to/zod"
import { useFormState, useFormStatus } from "react-dom"
import type { z } from "zod"

export type ConformStateType = SubmissionResult<string[]> | undefined

export type ConformServerAction = (
  previous: ConformStateType,
  formData: FormData
) => Promise<ConformStateType>

export const useConformAction = (
  serverAction: ConformServerAction,
  {
    schema,
    ...options
  /** formIdがomitしなきゃ必須になっちまう */ 
  }: Omit<FormOptions<z.output<z.ZodType>>, "formId"> & {
    schema: z.ZodType
  }
) => {
  const [lastResult, action] = useFormState(serverAction, undefined)

  const [form, fields] = useForm<z.output<z.ZodType>>({
    lastResult,
    onValidate({ formData }) {
      const parsed = parseWithZod(formData, { schema })
      return parsed
    },
    constraint: getZodConstraint(schema),
    ...options,
  })

  return [form, fields, action] as const
}

// component
export const Login = () => {
  const [form, fields, action] = useConformAction(
    async (prev: ConformStateType, action: FormData) => {
      const result = await loginAction(prev, action)
      return result
    },
    {
      schema: loginSchema,
      defaultValue: {
        username: "",
        password: "",
      },
    }
  )

  return (
    <form action={action} {...getFormProps(form)}>
      <AnyField error={fields.username.errors}>
        <Input
          placeholder="username"
          {...getInputProps(fields.username, { type: "text" })}
        />
      </AnyField>

      <AnyField error={fields.password.errors}>
        <Input
          placeholder="password"
          {...getInputProps(fields.password, { type: "password" })}
        />
      </AnyField>

      <ActionButton type="submit">
        送信
      </ActionButton>
    </form>
  )
}

// ActionButton
import { Button, ButtonProps } from "./Button"

export const ActionButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ disabled, ...props }, ref) => {
    const status = useFormStatus()

    return <Button disabled={disabled || status.pending} ref={ref} {...props} />
  }
)

ActionButton.displayName = "ActionButton"
  • useConformActionuseFormStateuseFormを包み込んでいます。useActionStateに完全に変更された時移行しやすいようにuseFormStateもいれるようにしてます。
  • componentからuseConformActionを呼び出して使うだけです。
  • ActionButtonuseFormStatusがformの中でレンダリングされていないと使えないので、Buttonをラップして使えるようにしています。

以上。

Discussion