Actions
Actions handle data mutations like creating, updating, and deleting records. They run in response to form submissions and programmatic submissions.Basic Action
Actions receive form data fromPOST, PUT, PATCH, or DELETE requests:
filename=app/routes/projects.new.tsx
import { redirect } from "react-router";
import type { Route } from "./+types/projects.new";
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const name = formData.get("name");
const description = formData.get("description");
const project = await db.project.create({
data: { name, description },
});
return redirect(`/projects/${project.id}`);
}
export default function NewProject() {
return (
<Form method="post">
<input type="text" name="name" required />
<textarea name="description" />
<button type="submit">Create Project</button>
</Form>
);
}
Using Form
The Form component triggers actions when submitted:
import { Form } from "react-router";
export default function DeleteProject({ loaderData }: Route.ComponentProps) {
return (
<Form method="post" action="/projects/delete">
<input type="hidden" name="id" value={loaderData.project.id} />
<button type="submit">Delete</button>
</Form>
);
}
Action Arguments
Actions receive the same arguments as loaders:export async function action({
request, // Fetch Request object
params, // URL parameters from the route path
context, // Router context
}: Route.ActionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
switch (intent) {
case "create":
return createItem(formData);
case "update":
return updateItem(params.id, formData);
case "delete":
return deleteItem(params.id);
default:
throw new Response("Invalid intent", { status: 400 });
}
}
Form Data Formats
URL Encoded (default)
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const email = formData.get("email");
const password = formData.get("password");
return await login(email, password);
}
JSON
import { Form } from "react-router";
export async function action({ request }: Route.ActionArgs) {
const data = await request.json();
return await createUser(data);
}
export default function Signup() {
return (
<Form method="post" encType="application/json">
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit">Sign Up</button>
</Form>
);
}
Multipart (file uploads)
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const file = formData.get("avatar") as File;
const buffer = await file.arrayBuffer();
const url = await uploadToStorage(buffer, file.name);
return { avatarUrl: url };
}
export default function UploadAvatar() {
return (
<Form method="post" encType="multipart/form-data">
<input type="file" name="avatar" accept="image/*" />
<button type="submit">Upload</button>
</Form>
);
}
Returning Data
Actions can return data to be used in the UI:export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const email = formData.get("email");
// Validate
if (!email || !email.includes("@")) {
return {
error: "Please enter a valid email address",
};
}
await subscribeToNewsletter(email);
return { success: true };
}
export default function Newsletter() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<input type="email" name="email" />
<button type="submit">Subscribe</button>
{actionData?.error && (
<p className="error">{actionData.error}</p>
)}
{actionData?.success && (
<p className="success">Subscribed!</p>
)}
</Form>
);
}
Using useActionData
Access action data with the useActionData hook:
import { Form, useActionData } from "react-router";
export async function action({ request }: Route.ActionArgs) {
const body = await request.formData();
const name = body.get("visitorsName");
return { message: `Hello, ${name}` };
}
export default function Greeting() {
const data = useActionData<typeof action>();
return (
<Form method="post">
<input type="text" name="visitorsName" />
{data ? data.message : "Waiting..."}
</Form>
);
}
Redirecting After Actions
import { redirect } from "react-router";
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const post = await db.post.create({
data: {
title: formData.get("title"),
content: formData.get("content"),
},
});
// Redirect to the new post
return redirect(`/posts/${post.id}`);
}
Multiple Actions per Form
Use button values to differentiate actions:export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "save") {
return await saveDraft(formData);
}
if (intent === "publish") {
return await publishPost(formData);
}
throw new Response("Invalid intent", { status: 400 });
}
export default function PostEditor() {
return (
<Form method="post">
<input type="text" name="title" />
<textarea name="content" />
<button type="submit" name="intent" value="save">
Save Draft
</button>
<button type="submit" name="intent" value="publish">
Publish
</button>
</Form>
);
}
Programmatic Submission
Submit forms programmatically withuseSubmit:
import { useSubmit } from "react-router";
export default function SearchBox() {
const submit = useSubmit();
return (
<Form
onChange={(e) => {
submit(e.currentTarget);
}}
>
<input type="text" name="q" />
</Form>
);
}
import { useSubmit } from "react-router";
export default function SaveButton() {
const submit = useSubmit();
const handleSave = () => {
submit(
{ title: "My Post", content: "..." },
{
method: "post",
encType: "application/json",
}
);
};
return <button onClick={handleSave}>Save</button>;
}
Navigation State
Track form submission state withuseNavigation:
import { Form, useNavigation } from "react-router";
export default function ContactForm() {
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<Form method="post">
<input type="email" name="email" />
<textarea name="message" />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Sending..." : "Send"}
</button>
</Form>
);
}
Error Handling
import { isRouteErrorResponse, useRouteError } from "react-router";
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const email = formData.get("email");
if (!email) {
throw new Response("Email is required", { status: 400 });
}
try {
await sendEmail(email);
return { success: true };
} catch (error) {
throw new Response("Failed to send email", { status: 500 });
}
}
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status}</h1>
<p>{error.data}</p>
</div>
);
}
return <div>An error occurred</div>;
}
Validation
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const email = formData.get("email");
const password = formData.get("password");
const errors: Record<string, string> = {};
if (!email || !email.includes("@")) {
errors.email = "Valid email is required";
}
if (!password || password.length < 8) {
errors.password = "Password must be at least 8 characters";
}
if (Object.keys(errors).length > 0) {
return { errors };
}
return await createAccount({ email, password });
}
export default function Signup() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<div>
<input type="email" name="email" />
{actionData?.errors?.email && (
<span className="error">{actionData.errors.email}</span>
)}
</div>
<div>
<input type="password" name="password" />
{actionData?.errors?.password && (
<span className="error">{actionData.errors.password}</span>
)}
</div>
<button type="submit">Sign Up</button>
</Form>
);
}
Client Actions
UseclientAction for client-only mutations:
export async function action({ request }: Route.ActionArgs) {
// Runs on server
const formData = await request.formData();
return await updateUser(formData);
}
export async function clientAction({ serverAction }: Route.ClientActionArgs) {
// Runs on client
const result = await serverAction();
// Update client cache
cache.invalidate('user');
return result;
}
Related
- Loaders - Loading data
- Fetchers - Mutations without navigation
- Optimistic UI - Update UI before action completes