Remixでデータの更新を行う際に利用するactionですが、1つのリソースに対する複数の更新操作やユースケースごとの更新操作をまとめたくなる場面があります。例えばTODOアプリケーションであれば、アイテムの追加・更新・削除を行いたいケースが考えられます。このような場面でzodのdiscriminatedUnionを使うと便利です。
サンプル実装
追加・更新・削除の操作時に要求されるデータは異なるため、それぞれのスキーマを用意しておきます。
各操作の種類を区別するためのフィールド(以下の例ではcommand
)を設定し、それぞれの操作に要求されるスキーマを定義します。
import { z } from 'zod'; export const CreateTodoSchema = z.object({ command: z.literal('create'), item: z.object({ title: z.string(), description: z.string().optional(), status: z.enum(['notStarted', 'inProgress', 'pending', 'done']), }), }); export const EditTodoSchema = z.object({ command: z.literal('edit'), item: z .object({ title: z.string(), description: z.string(), status: z.enum(['notStarted', 'inProgress', 'pending', 'done']), }) .partial(), }); export const DeleteTodoSchema = z.object({ command: z.literal('delete'), });
上記で定義したスキーマを組み合わせてrequest bodyのパースを行います。ここでzodのdiscriminatedUnion
の出番です。discriminatedUnion
を使うことで、以下のようにスキーマを構成することができます。
import { z } from 'zod'; export async function action({ request }: ActionFunctionArgs) { // Content-Type が application/json の場合 const body = await request.json(); const ret = z .discriminatedUnion('command', [ CreateTodoSchema, EditTodoSchema, DeleteTodoSchema, ]) .safeParse(body); if (!ret.success) { throw new Error('Unknown form schema'); } if (ret.data.command === 'create') { return createTodo(ret.data); } if (ret.data.command === 'edit') { return updateTodo(ret.data); } if (ret.data.command === 'delete') { return deleteTodo(ret.data); } throw new Error('Unknown command'); }
jsonではなくformDataの場合はzodに渡す前にデータの変換が必要になりますが、RVFやConformなどのフォームバリデーションライブラリを使えばその辺りも簡素に書くことができます。