Skip to content
>_devvkit
$devvkit learn --librarie remix-guide

Remix Guide

[react][ssr][full-stack][remix]
JavaScript / TypeScript
Install
npx create-remix@latest
# or: npm create remix@latest my-app

Remix embraces web fundamentals. Instead of client-side data fetching, you export loader and action functions that run on the server. Nested routes load data in parallel, and only the parts of the page that change re-render.

Remix is built on React Router v7. Routes use outlet-based nesting — parent layouts remain mounted when children change. Form handling uses standard HTML forms with progressive enhancement via useFetcher.

Remix v3 (React Router v7) merges the two projects. You get Remix's data patterns with React Router's flexibility. Deploy to Vercel, Cloudflare, Fly.io, or any Node.js host.

Setup

Create projectScaffold Remix app.
npx create-remix@latest my-app --template remix-run/indie-stack
cd my-app
npm run dev

Routing

Route with loaderServer data loading.
// app/routes/users._index.tsx
export async function loader({ request }: LoaderFunctionArgs) {
  const users = await db.findMany()
  return json({ users })
}

export default function Users() {
  const { users } = useLoaderData<typeof loader>()
  return <div>{users.map(u => <p key={u.id}>{u.name}</p>)}</div>
}
Nested routeParent layout with outlet.
// app/routes/users.tsx (parent layout)
export default function UsersLayout() {
  return (
    <div>
      <Sidebar />
      <Outlet />
    </div>
  )
}

// app/routes/users..tsx (child)
export async function loader({ params }: LoaderFunctionArgs) {
  const user = await db.findUnique({ where: { id: params.id } })
  return json({ user })
}
Error boundaryRoute-level error handling.
export function ErrorBoundary({ error }: { error: Error }) {
  return (
    <div>
      <h1>Oops!</h1>
      <p>{error.message}</p>
    </div>
  )
}
Resource routeNon-page API endpoint.
// app/routes/api.users.ts (no default export)
export async function loader({ request }: LoaderFunctionArgs) {
  const users = await db.findMany()
  return json(users)
}

Actions

Form actionServer mutation.
export async function action({ request }: ActionFunctionArgs) {
  const form = await request.formData()
  const name = form.get('name') as string
  '---'
  await db.user.create({ data: { name } })
  return redirect('/users')
}

export default function NewUser() {
  return (
    <Form method="post">
      <input name="name" />
      <button type="submit">Create</button>
    </Form>
  )
}

Forms

useFetcherProgressive enhancement.
const fetcher = useFetcher()

<fetcher.Form method="post" action="/users">
  <input name="name" />
  <button type="submit">{fetcher.state === 'submitting' ? 'Saving...' : 'Save'}</button>
</fetcher.Form>