Copy-paste Claude Code prompts for Remix — loaders, actions, nested routes, Conform forms, and pessimistic vs optimistic UI patterns. React Router 7 ready.
Remix's loader/action model maps cleanly to how Claude thinks about server work. The trick is keeping it from importing client-side libraries into loaders — a common slip. The prompts below add explicit guardrails for the server/client split.
Add a nested route /projects/$projectId/settings.
Files needed:
- app/routes/projects.$projectId.settings.tsx (single file route)
- Loader: fetch the project via the existing app/services/projects.server.ts
getProjectForUser(projectId, userId). Throw redirect('/projects') if not found.
- Meta: title = `Settings — ${project.name}`
- Action: handle 'rename' and 'archive' intents (separate buttons in the form)
- Form: Conform schema in app/schemas/project-settings.ts
Constraints:
- Imports from .server.ts files only in loader/action — never in the component.
- Use useLoaderData and Form (no client-side fetch).
- On rename success: return redirect(`/projects/${id}/settings?renamed=1`).
- On archive: redirect('/projects?archived=1').
- Read app/routes/projects.$projectId.tsx FIRST as the template.
The /projects index renders a list. Adding a project currently round-trips
and feels slow. Convert it to optimistic UI.
Constraints:
- Keep the form action — don't switch to a JSON endpoint.
- In the route component, use useFetcher() for the form submit.
- Compute optimistic items from fetcher.formData when fetcher.state !== 'idle'.
- Render the optimistic item in the list with reduced opacity (style.opacity=0.5)
and a small spinner.
- After action completes, the loader revalidation replaces the optimistic
item with the real one — verify that the keys align so React doesn't
remount.
- Add a Playwright spec that types a name, hits Enter, and asserts the item
appears in the list within 100ms (well before the network would resolve).
The /dashboard loader is slow because it fetches the recent activity feed,
which takes 800ms. The other dashboard data is fast.
Refactor the loader to:
- Return critical data immediately (json or in the defer object resolved)
- defer({ activity: getActivityFeed(userId) }) — passed as a promise
- In the component, use with a Suspense
fallback showing a skeleton
Constraints:
- Use the existing Skeleton component (app/components/Skeleton.tsx)
- Error boundary inside — show "Couldn't load activity" with a
retry that calls revalidator.revalidate()
- Don't change the loader for other routes — only /dashboard.
## Stack
Remix 2 (Vite), React 19, Conform forms, Drizzle on Postgres,
Tailwind v4, Playwright.
## Conventions
- Server-only code in *.server.ts(x). Never import in components.
- Forms via
Related: testing prompts.