Remix Validated Form で POST action 正常終了で完了画面の TypeScript 型判定は型ガード必要

2023/05/10に公開1

結論

こんな感じの型ガードをフォームごとに作るのが一番マシ

export const isSuccessResult = (result: unknown): result is { success: true } =>
  !!result &&
    typeof result === 'object' &&
    'success' in result &&
    result.success === true

こんにちは。最近また推しである Remix 再評価の雰囲気が出てきて嬉しい coji です。

Remix Validated Form 便利ですよね。
その便利なのを使って、問い合わせフォーム作ってて、登録が完了したときに「ありがとうございました」って出したいとか、よくあります。

だけど、サーバ側でバリデーションチェックをしつつ、正常終了を useActionData でとってきて確認して、完了画面を出すをやりたかったのですが、こうなっちゃうんですよ。

再現コードはこちら。

contact.tsx
import { type ActionArgs, json } from '@remix-run/node'
import { useActionData } from '@remix-run/react'
import { ValidatedForm } from 'remix-validated-form'
import { withZod } from '@remix-validated-form/with-zod'
import { z } from 'zod'

// サーバ・クライアント共通で使う validator
const validator = withZod(z.object({
  name: z.string().min(1, '必須').max(50, '50文字以内です'),
  email: z.string().min(1, { message: '必須' }).max(100, '100文字以内です').email('メールアドレスでなければなりません'),
  message: z.string().min(1, { message: '必須' }).max(10000, '10000文字以内です'),
}))

// action
export const action = async ({request}: ActionArgs) => {
  // サーバ側でのバリデーションチェック
  const result = await validator.validate(await request.formData())
  if (result.error) {
    return validationError(result.error) // エラー
  }

  // メール送ったりなんなりする
  
  // 成功
  return json({success: true})
}

// フォーム
export default function ContactFormPage() {
  const actionData = useActionData<typeof action>()

  // action が成功してたら完了表示にしたい
  if (actionData?.data.success) { // <==!!!
    return <div>お問い合わせありがとうございました。追ってご連絡しますのでおまちください。</div>
  }

  return (
    <ValidatedForm method="POST" validator={validator}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" />
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" />
      <label htmlFor="name">Message</label>
      <textarea id="message" name="message" />
      <button type="submit">submit</button>
    </ValidatedForm>
  )
}

まあ action の中で validationError も返してるから、型を絞れてないってことなんですよね。
それで、仕方なく以下のようにしたりしますが、もうこれすっごくやだな〜ともってたんですよ。

  // action が成功してたら完了表示にしたい
  if (typeof actionData === 'object' && 'success' in actionData && actionData.success === true) { // <==!!!
    return <div>お問い合わせありがとうございました。追ってご連絡しますのでおまちください。</div>
  }

で、Remix Validated Form のイシューみたら、Author の方がこんな回答してるのを見つけましてね。

Your action return both validationError & json('...'), which means you will have to use type guard in order to get the type works.

https://github.com/airjp73/remix-validated-form/issues/140#issuecomment-1428085853

ああ、やっぱりそうなのか〜。と悲しみにくれたという落ちでした。

ずっとなんとかならないかと思ってたんですが、まあ、author の方がそうおっしゃるなら、まあそれが一番よいのだろうなあ。他に思いつかないし。ということだけを記録したかったので記事にしたのでした。

まあこれはちょっと残念ではあるんですが、全体的にそういうところも含めて Remix は推せるので、推していきます。

Discussion

melodycluemelodyclue

この問題に辿り着いてこの記事を見ました。解決策は無理矢理感あってちょっと残念でしたが、逆に言えばガードするだけなので良しです笑