Supercharge your remix loaders and actions with conform and zod.
The API and design are heavily inspired by remix-easy-mode by Samuel Cook. You can find his repo here: https://github.com/sjc5/remix-easy-mode
Special thanks to Kiliman for providing the utilities for param and query parsing: remix-params-helper by kiliman
Form data parsing is done using conform by edmundhung
Install the package and required peer dependencies
npm install remix-conform-rpc zod remix-params-helper @conform-to/react @conform-to/zod @conform-to/dom
yarn add remix-conform-rpc zod remix-params-helper @conform-to/react @conform-to/zod @conform-to/dom
You can define a loader by calling the setupLoader
function and passing an object with a load
function.
import { setupLoader } from "remix-conform-rpc/server/loader";
export const loader = (loaderArgs: LoaderFunctionArgs) => setupLoader({
loaderArgs,
load: async ({ context, request }) => {
return { message: "hello world" };
}
});
You can add type-safe query and param parsing by using the paramSchema
and/or querySchema
props.
Once you define a param or query schema, the object becomes available in the params
and query
object in the load
function.
import { setupLoader } from "remix-conform-rpc/server/loader";
import { z } from "zod";
export const loader = (loaderArgs: LoaderFunctionArgs) => setupLoader({
loaderArgs,
querySchema: z.object({
page: z.coerce.number().optional()
}),
paramSchema: z.object({
id: z.string()
}),
load: async ({ context, request, params, query }) => {
params.id; // string - typesafe
query.page; // number | undefined - typesafe
return { message: "hello world" };
}
});
You can run middleware before your loader. Anything you return from your middleware will be available in the load
functions arguments.
import { setupLoader } from "remix-conform-rpc/server/loader";
import { z } from "zod";
export const loader = (loaderArgs: LoaderFunctionArgs) => setupLoader({
loaderArgs,
middleware: async ({ context, request }) => {
const user = await getUserFromSession(request);
return { user };
},
load: async ({ context, request, user }) => {
user; // user object returned from middleware
return { message: "hello world" };
}
});
Define a loader with a zod schema to parse and validate the form data body.
import { setupAction } from "remix-conform-rpc/server/action";
import { z } from "zod";
export const action = (actionArgs: ActionFunctionArgs) => setupAction({
actionArgs,
schema: z.object({
email: z.string().email(),
password: z.string().min(8)
}),
mutation: async ({ request, submission }) => {
//Already validated and parsed
const { email, password } = submission.value;
}
});
Note
If the submission validation fails, the following object will be returned from your action (with http status 400):
{
"error": "invalid_submission",
"status": "error",
"code": 400,
"result": {}
//conform submission reply with errors
}
The same way you can define loaders, you can define actions with params, query and middleware.
import { setupAction } from "remix-conform-rpc/server/action";
import { z } from "zod";
export const action = (actionArgs: ActionFunctionArgs) => setupAction({
actionArgs,
schema: z.object({
email: z.string().email(),
password: z.string().min(8)
}),
querySchema: z.object({
page: z.coerce.number().optional()
}),
paramSchema: z.object({
id: z.string()
}),
middleware: async ({ context, request, params }) => {
const user = await getUserFromSession(request);
await checkUserPermissions(user, params.id);
return { user };
},
mutation: async ({ request, submission, user, query, params }) => {
return { message: "hello world" };
}
});
While you can use standard html forms to submit data, you can also enhance your users experience with the useAction
hook.
import { useAction } from "remix-conform-rpc/hooks/action";
import { z } from "zod";
const formSchema = z.object({
name: z.string(),
description: z.string().optional()
});
const { submit, fetcher } = useAction<typeof action, typeof formSchema>({
//all options are optional
path: "/api/products",
method: "post",
onSuccess: (actionResult) => {
//do something with the result
},
onError: (errorResult) => {
const data = errorResult.result; //Return from the server
const status = errorResult.status; //"error"
const statusCode = errorResult.code; // http status code
const errorMessage = errorResult.error; // error message from the server
}
});
//Parameters and types are automatically inferred
submit({
name: "Product name",
description: "Product description"
});
You can also leverage typesafe form creating using the useActionForm
hook.
import { useActionForm } from "remix-conform-rpc/hooks/action";
import { z } from "zod";
const formSchema = z.object({
name: z.string(),
description: z.string().optional()
});
const { form, fields, submit, fetcher } = useActionForm<typeof action, typeof formSchema>({
//All options are optional
onSuccess: (actionResult) => {
//do something with the result
},
onError: (errorResult) => {
const data = errorResult.result; //Return from the server
const status = errorResult.status; //"error"
const statusCode = errorResult.code; // http status code
const errorMessage = errorResult.error; // error message from the server
},
onSubmit: (event, { name, description }) => {
event.preventDefault();
submit({ name, description });
},
defaultValue: {
name: "My product",
description: "My product description"
}
});
See the conform documentation for more information on how to use the form
and
fields
objects