Remix Cheat Sheet
A reference for Remix's loaders, actions, nested file-based routing, and form-driven mutations for building server-rendered React apps.
Loaders
Fetching data on the server before a route renders.
// app/routes/posts.$postId.tsximport { json } from '@remix-run/node';import { useLoaderData } from '@remix-run/react';export async function loader({ params }) { const post = await getPost(params.postId); if (!post) throw new Response('Not Found', { status: 404 }); return json({ post });}export default function Post() { const { post } = useLoaderData(); return <h1>{post.title}</h1>;}
Actions & Forms
Handling mutations with progressively-enhanced forms.
import { redirect } from '@remix-run/node';import { Form } from '@remix-run/react';export async function action({ request }) { const formData = await request.formData(); const title = formData.get('title'); const post = await createPost({ title }); return redirect(`/posts/${post.id}`);}export default function NewPost() { return ( <Form method='post'> <input name='title' /> <button type='submit'>Create</button> </Form> );}
Core Concepts
The route-level APIs Remix is built around.
- loader- server function that runs before render to fetch data for a route
- action- server function that handles form submissions and mutations (POST/PUT/DELETE)
- useLoaderData- hook that reads the data returned by a route's loader
- useActionData- hook that reads the data returned by a route's action, e.g. validation errors
- useFetcher- hook for loading or submitting data without triggering a full navigation
- ErrorBoundary- route-level component rendered when a loader, action, or render throws
- Nested routes- composed via file naming (e.g. posts.$postId.tsx) and rendered through <Outlet />
File-Based Route Naming
How file names map to URL paths in Remix's flat routes convention.
app/routes/_index.tsx # -> "/"app/routes/posts._index.tsx # -> "/posts"app/routes/posts.$postId.tsx # -> "/posts/:postId"app/routes/posts.new.tsx # -> "/posts/new"app/root.tsx # root layout, renders <Outlet />, <Scripts />, <Links />
Streaming Slow Data with defer()
Return fast-loading data immediately and stream in slower data later, rendering a fallback with Suspense in the meantime.
import { defer } from '@remix-run/node';import { Await, useLoaderData } from '@remix-run/react';import { Suspense } from 'react';export function loader({ params }) { const criticalData = getPostMeta(params.postId); // awaited const slowComments = getComments(params.postId); // NOT awaited, a promise return defer({ post: criticalData, commentsPromise: slowComments });}export default function Post() { const { post, commentsPromise } = useLoaderData(); return ( <> <h1>{post.title}</h1> <Suspense fallback={<p>Loading comments...</p>}> <Await resolve={commentsPromise}> {(comments) => <CommentList comments={comments} />} </Await> </Suspense> </> );}
Optimistic UI with useFetcher
Submit a mutation without navigating, and read fetcher.formData to render the pending result before the server responds.
import { useFetcher } from '@remix-run/react';function LikeButton({ postId, liked }) { const fetcher = useFetcher(); // fall back to the real value once the fetcher is idle again const optimisticLiked = fetcher.formData ? fetcher.formData.get('liked') === 'true' : liked; return ( <fetcher.Form method='post' action={`/posts/${postId}/like`}> <input type='hidden' name='liked' value={String(!optimisticLiked)} /> <button type='submit' disabled={fetcher.state !== 'idle'}> {optimisticLiked ? 'Liked' : 'Like'} </button> </fetcher.Form> );}
meta() and headers() Route Exports
Route modules can export functions that compute page metadata and HTTP response headers from loader data.
export const meta = ({ data }) => [ { title: data?.post ? `${data.post.title} | Blog` : 'Not Found' }, { name: 'description', content: data?.post?.excerpt },];export const headers = ({ loaderHeaders, parentHeaders }) => ({ 'Cache-Control': loaderHeaders.get('Cache-Control') ?? 'max-age=60',});export function loader() { return json( { post }, { headers: { 'Cache-Control': 'public, max-age=300' } } );}
Advanced Hooks & Route Behavior
APIs for pending-state UI, error handling, and controlling when Remix re-runs loaders.
- useNavigation- reports the app's pending navigation state ('idle'|'loading'|'submitting') for global or per-link pending UI
- useRevalidator- manually triggers a revalidation of all active route loaders, e.g. after a WebSocket event
- useRouteError / isRouteErrorResponse- read the thrown error in an ErrorBoundary and narrow it to a Response vs. a plain JS error
- shouldRevalidate- route export that opts a loader out of re-running after certain actions/navigations, for performance
- clientLoader / clientAction- run in the browser instead of on the server, enabling client-side caching or skipping the server round-trip
- resource routes- routes with no default export, used to serve JSON, RSS, images, or webhooks from the same routing system
- useMatches- returns data and handles for every matched route in the current tree, useful for breadcrumbs
- single fetch- Remix's newer data-loading strategy that batches all loader calls into one HTTP request per navigation
Remix automatically re-runs a route's loaders after any action on the page completes, so you rarely need manual client-side cache invalidation -- just return the mutated data from the action (or nothing) and let Remix's built-in revalidation refresh the UI.