Form validation and errors
A user can submit an empty form. Validation happens in the action on the server: if the data is not acceptable, the action does not save the post and returns a list of errors instead. The component receives that response in the actionData prop and shows the errors next to the fields.
Returning errors
Extend the action in app/routes/new-post.tsx:
// app/routes/new-post.tsx
import { Form, data, redirect } from "react-router";
import type { Route } from "./+types/new-post";
import { createPost } from "../data.server";
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const title = String(formData.get("title") ?? "").trim();
const body = String(formData.get("body") ?? "").trim();
const errors: { title?: string; body?: string } = {};
if (title.length < 3) errors.title = "Title must be at least 3 characters";
if (body === "") errors.body = "Write the post text";
if (Object.keys(errors).length > 0) {
return data({ errors }, { status: 400 });
}
const post = await createPost(title, body);
return redirect(`/posts/${post.id}`);
}The data function from react-router returns an object together with an HTTP status: 400 means "bad request". Without a status you can return plain { errors } too.
Showing the errors
// app/routes/new-post.tsx
export default function NewPost({ actionData }: Route.ComponentProps) {
const errors = actionData?.errors;
return (
<Form method="post">
<input name="title" placeholder="Title" />
{errors?.title && <p className="error">{errors.title}</p>}
<textarea name="body" placeholder="Post text" />
{errors?.body && <p className="error">{errors.body}</p>}
<button type="submit">Publish</button>
</Form>
);
}Submit an empty form — "Title must be at least 3 characters" and "Write the post text" appear under the fields. Before the first submission actionData is undefined, so it is accessed with ?..
useActionData
Only the route component has the actionData prop. If the form is split into separate components, a nested component gets the same object with a hook:
// app/components/FieldError.tsx
import { useActionData } from "react-router";
export function TitleError() {
const actionData = useActionData<{ errors?: { title?: string } }>();
return actionData?.errors?.title ? <p>{actionData.errors.title}</p> : null;
}Checking in the browser
The required and minLength attributes give quick feedback before submission. They do not replace server validation: a request can be sent bypassing the form.
Common mistake
After a validation error the form fields are empty, and the user has to type everything again. This happens when the form is reset or the fields are controlled by state that gets cleared. With Form and uncontrolled fields the typed text stays in place; if fields are reset anyway, return the values from the action together with the errors, for example data({ errors, values: { title, body } }, { status: 400 }), and put them into defaultValue.