├── README.md
├── LICENSE
└── .cursor
└── rules
└── react-router-v7.mdc
/README.md:
--------------------------------------------------------------------------------
1 | # React Router Cursor Rules
2 |
3 | Some cursor rules that make it easier to use React Router with Framework mode
4 |
5 | [Read them](./.cursor/rules/react-router-v7.mdc)
6 |
7 | If you prefer, you can [watch a video walkthrough of these rules in action](https://www.youtube.com/watch?v=gkBjxB_3kDs)
8 |
9 | Have a suggestion for improving these rules? Open up an issue or PR!
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2025 Brooks Lybrand
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/.cursor/rules/react-router-v7.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: true
5 | ---
6 | # React Router v7 Framework Mode - Cursor Rules
7 |
8 | ## 🚨 CRITICAL: Route Type Imports - NEVER MAKE THIS MISTAKE
9 |
10 | **THE MOST IMPORTANT RULE: ALWAYS use `./+types/[routeName]` for route type imports.**
11 |
12 | ```tsx
13 | // ✅ CORRECT - ALWAYS use this pattern:
14 | import type { Route } from "./+types/product-details";
15 | import type { Route } from "./+types/product";
16 | import type { Route } from "./+types/category";
17 |
18 | // ❌ NEVER EVER use relative paths like this:
19 | // import type { Route } from "../+types/product-details"; // WRONG!
20 | // import type { Route } from "../../+types/product"; // WRONG!
21 | ```
22 |
23 | **If you see TypeScript errors about missing `./+types/[routeName]` modules:**
24 | 1. **IMMEDIATELY run `typecheck`** to generate the types
25 | 2. **Or start the dev server** which will auto-generate types
26 | 3. **NEVER try to "fix" it by changing the import path**
27 |
28 | ## Type Generation & Workflow
29 |
30 | - **Run `typecheck` after adding/renaming any routes**
31 | - **Run `typecheck` if you see missing type errors**
32 | - Types are auto-generated by `@react-router/dev` in `./+types/[routeName]` relative to each route file
33 | - **The dev server will also generate types automatically**
34 |
35 | ---
36 |
37 | ## Critical Package Guidelines
38 |
39 | ### ✅ CORRECT Packages:
40 | - `react-router` - Main package for routing components and hooks
41 | - `@react-router/dev` - Development tools and route configuration
42 | - `@react-router/node` - Node.js server adapter
43 | - `@react-router/serve` - Production server
44 |
45 | ### ❌ NEVER Use:
46 | - `react-router-dom` - Legacy package, use `react-router` instead
47 | - `@remix-run/*` - Old packages, replaced by `@react-router/*`
48 | - React Router v6 patterns - Completely different architecture
49 |
50 | ## Essential Framework Architecture
51 |
52 | ### Route Configuration (`app/routes.ts`)
53 | ```tsx
54 | import { type RouteConfig, index, route } from "@react-router/dev/routes";
55 |
56 | export default [
57 | index("routes/home.tsx"),
58 | route("about", "routes/about.tsx"),
59 | route("products/:id", "routes/product.tsx", [
60 | index("routes/product-overview.tsx"),
61 | route("reviews", "routes/product-reviews.tsx"),
62 | ]),
63 | route("categories", "routes/categories-layout.tsx", [
64 | index("routes/categories-list.tsx"),
65 | route(":slug", "routes/category-details.tsx"),
66 | ]),
67 | ] satisfies RouteConfig;
68 | ```
69 |
70 | ### Route Module Pattern (`app/routes/product.tsx`)
71 | ```tsx
72 | import type { Route } from "./+types/product";
73 |
74 | // Server data loading
75 | export async function loader({ params }: Route.LoaderArgs) {
76 | return { product: await getProduct(params.id) };
77 | }
78 |
79 | // Client data loading (when needed)
80 | export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) {
81 | // runs on the client and is in charge of calling the loader if one exists via `serverLoader`
82 | const serverData = await serverLoader();
83 | return serverData
84 | }
85 |
86 | // Form handling
87 | export async function action({ request }: Route.ActionArgs) {
88 | const formData = await request.formData();
89 | await updateProduct(formData);
90 | return redirect(href("/products/:id", { id: params.id }));
91 | }
92 |
93 | // Component rendering
94 | export default function Product({ loaderData }: Route.ComponentProps) {
95 | return
{loaderData.product.name}
;
96 | }
97 | ```
98 |
99 | ### Layout/Parent Routes with Outlet
100 | **For layout routes that have child routes, ALWAYS use `` to render child routes:**
101 |
102 | ```tsx
103 | import type { Route } from "./+types/categories-layout";
104 | import { Outlet } from "react-router";
105 |
106 | export default function CategoriesLayout(props: Route.ComponentProps) {
107 | return (
108 |
109 |
112 |
113 | {/* ✅ This renders the matching child route */}
114 |
115 |
116 | );
117 | }
118 |
119 | // ❌ Never use `children` from the component props, it doesn't exist
120 | // export default function CategoriesLayout({ children }: Route.ComponentProps) {
121 |
122 |
123 | ## Automatic Type Safety & Generated Types
124 |
125 | **React Router v7 automatically generates types for every route.** These provide complete type safety for loaders, actions, components, and URL generation.
126 |
127 | ### ✅ ALWAYS Use Generated Types:
128 | Types are autogenerated and should be imported as `./+types/[routeFileName]`. **If you're getting a type error, run `npm run typecheck` first.**
129 |
130 | The filename for the autogenerated types is always a relative import of `./+types/[routeFileName]`:
131 |
132 | ```tsx
133 | // routes.ts
134 | route("products/:id", "routes/product-details.tsx")
135 |
136 | // routes/product-details.tsx
137 | // ✅ CORRECT: Import generated types for each route
138 | import type { Route } from "./+types/product-details";
139 |
140 | export async function loader({ params }: Route.LoaderArgs) {
141 | // params.id is automatically typed based on your route pattern
142 | return { product: await getProduct(params.id) };
143 | }
144 |
145 | export default function ProductDetails({ loaderData }: Route.ComponentProps) {
146 | // loaderData.product is automatically typed from your loader return
147 | return
{loaderData.product.name}
;
148 | }
149 | ```
150 |
151 |
152 | ### ✅ Type-Safe URL Generation with href():
153 | ```tsx
154 | import { Link, href } from "react-router";
155 |
156 | // Static routes
157 | New Product
158 |
159 | // Dynamic routes with parameters - AUTOMATIC TYPE SAFETY
160 | View Product
161 | Edit Product
162 |
163 | // Works with redirects too
164 | return redirect(href("/products/:id", { id: newProduct.id }));
165 | ```
166 |
167 | ### ❌ NEVER Create Custom Route Types:
168 | ```tsx
169 | // ❌ DON'T create custom type files for routes
170 | export namespace Route {
171 | export interface LoaderArgs { /* ❌ */ }
172 | export interface ComponentProps { /* ❌ */ }
173 | }
174 |
175 | // ❌ DON'T manually construct URLs - no type safety
176 | Product // ❌
177 | Product // ❌
178 | ```
179 |
180 | ### Type Generation Setup:
181 | - **Location**: Types generated in `./+types/[routeName]` relative to each route file
182 | - **Auto-generated**: Created by `@react-router/dev` when you run dev server or `npm run typecheck`
183 | - **Comprehensive**: Covers `LoaderArgs`, `ActionArgs`, `ComponentProps`, `ErrorBoundaryProps`
184 | - **TypeScript Config**: Add `.react-router/types/**/*` to `include` in `tsconfig.json`
185 |
186 | ## Critical Imports & Patterns
187 |
188 | ### ✅ Correct Imports:
189 | ```tsx
190 | import { Link, Form, useLoaderData, useFetcher, Outlet } from "react-router";
191 | import { type RouteConfig, index, route } from "@react-router/dev/routes";
192 | import { data, redirect, href } from "react-router";
193 | ```
194 |
195 | ## Data Loading & Actions
196 |
197 | ### Server vs Client Data Loading:
198 | ```tsx
199 | // Server-side rendering and pre-rendering
200 | export async function loader({ params }: Route.LoaderArgs) {
201 | return { product: await serverDatabase.getProduct(params.id) };
202 | }
203 |
204 | // Client-side navigation and SPA mode
205 | export async function clientLoader({ params }: Route.ClientLoaderArgs) {
206 | return { product: await fetch(`/api/products/${params.id}`).then(r => r.json()) };
207 | }
208 |
209 | // Use both together - server for SSR, client for navigation
210 | clientLoader.hydrate = true; // Force client loader during hydration
211 | ```
212 |
213 | ### Form Handling & Actions:
214 | ```tsx
215 | // Server action
216 | export async function action({ request }: Route.ActionArgs) {
217 | const formData = await request.formData();
218 | const result = await updateProduct(formData);
219 | return redirect(href("/products"));
220 | }
221 |
222 | // Client action (takes priority if both exist)
223 | export async function clientAction({ request }: Route.ClientActionArgs) {
224 | const formData = await request.formData();
225 | await apiClient.updateProduct(formData);
226 | return { success: true };
227 | }
228 |
229 | // In component
230 |
235 | ```
236 |
237 | ## Navigation & Links
238 |
239 | ### Basic Navigation:
240 | ```tsx
241 | import { Link, NavLink } from "react-router";
242 |
243 | // Simple links
244 | Products
245 |
246 | // Active state styling
247 |
248 | isActive ? "active" : ""
249 | }>
250 | Dashboard
251 |
252 |
253 | // Programmatic navigation
254 | const navigate = useNavigate();
255 | navigate("/products");
256 | ```
257 |
258 | ### Advanced Navigation with Fetchers:
259 | ```tsx
260 | import { useFetcher } from "react-router";
261 |
262 | function AddToCartButton({ productId }: { productId: string }) {
263 | const fetcher = useFetcher();
264 |
265 | return (
266 |
267 |
268 |
271 |
272 | );
273 | }
274 | ```
275 |
276 | ## File Organization & Naming
277 |
278 | ### ✅ Flexible File Naming:
279 | React Router v7 uses **explicit route configuration** in `app/routes.ts`. You are NOT constrained by old file-based routing conventions.
280 |
281 | ```tsx
282 | // ✅ Use descriptive, clear file names
283 | export default [
284 | route("products", "routes/products-layout.tsx", [
285 | index("routes/products-list.tsx"),
286 | route(":id", "routes/product-details.tsx"),
287 | route(":id/edit", "routes/product-edit.tsx"),
288 | ]),
289 | ] satisfies RouteConfig;
290 | ```
291 |
292 | ### File Naming Best Practices:
293 | - Use **descriptive names** that clearly indicate purpose
294 | - Use **kebab-case** for consistency (`product-details.tsx`)
295 | - Organize by **feature** rather than file naming conventions
296 | - The **route configuration** is the source of truth, not file names
297 |
298 | ## Error Handling & Boundaries
299 |
300 | ### Route Error Boundaries:
301 | Only setup `ErrorBoundary`s for routes if the users explicitly asks. All errors bubble up to the `ErrorBoundary` in `root.tsx` by default.
302 |
303 | ```tsx
304 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
305 | if (isRouteErrorResponse(error)) {
306 | return (
307 |
308 |
{error.status} {error.statusText}
309 |
{error.data}
310 |
311 | );
312 | }
313 |
314 | return (
315 |
316 |
Oops!
317 |
{error.message}
318 |
319 | );
320 | }
321 | ```
322 |
323 | ### Throwing Errors from Loaders/Actions:
324 | ```tsx
325 | export async function loader({ params }: Route.LoaderArgs) {
326 | const product = await db.getProduct(params.id);
327 | if (!product) {
328 | throw data("Product Not Found", { status: 404 });
329 | }
330 | return { product };
331 | }
332 | ```
333 |
334 | ## Advanced Patterns
335 |
336 | ### Pending UI & Optimistic Updates:
337 | ```tsx
338 | import { useNavigation, useFetcher } from "react-router";
339 |
340 | // Global pending state
341 | function GlobalSpinner() {
342 | const navigation = useNavigation();
343 | return navigation.state === "loading" ? : null;
344 | }
345 |
346 | // Optimistic UI with fetchers
347 | function CartItem({ item }) {
348 | const fetcher = useFetcher();
349 | const quantity = fetcher.formData
350 | ? parseInt(fetcher.formData.get("quantity"))
351 | : item.quantity;
352 |
353 | return (
354 |
355 | fetcher.submit(e.currentTarget.form)}
360 | />
361 | {item.product.name}
362 |
363 | );
364 | }
365 | ```
366 |
367 | ### Progressive Enhancement:
368 | ```tsx
369 | // Works without JavaScript, enhanced with JavaScript
370 | export default function ProductSearchForm() {
371 | return (
372 |
376 | );
377 | }
378 | ```
379 |
380 | ## Anti-Patterns to Avoid
381 |
382 | ### ❌ React Router v6 Patterns:
383 | ```tsx
384 | // DON'T use component routing
385 |
386 | } />
387 |
388 | ```
389 |
390 | ### ❌ Manual Data Fetching:
391 | ```tsx
392 | // DON'T fetch in components
393 | function Product() {
394 | const [data, setData] = useState(null);
395 | useEffect(() => { fetch('/api/products') }, []);
396 | // Use loader instead!
397 | }
398 | ```
399 |
400 | ### ❌ Manual Form Handling:
401 | ```tsx
402 | // DON'T handle forms manually
403 | const handleSubmit = (e) => {
404 | e.preventDefault();
405 | fetch('/api/products', { method: 'POST' });
406 | };
407 | // Use Form component and action instead!
408 | ```
409 |
410 | ## Essential Type Safety Rules
411 |
412 | 1. **ALWAYS** import from `"./+types/[routeName]"` - never use relative paths like `"../+types/[routeName]"`
413 | 2. **RUN `npm run typecheck`** when you see missing type errors - never try to "fix" the import path
414 | 3. **ALWAYS** use `href()` for dynamic URLs - never manually construct route strings
415 | 4. **LET TypeScript infer** loader/action return types - don't over-type returns
416 | 5. **USE Route.ComponentProps** for your route components - automatic loaderData typing
417 | 6. **ADD** `.react-router/types/**/*` to your `tsconfig.json` include array
418 |
419 | ## AI Assistant Guidelines
420 |
421 | When working with React Router v7:
422 | - **If you see missing `./+types/[routeName]` imports, ALWAYS suggest running `npm run typecheck` first**
423 | - **NEVER suggest changing `./+types/[routeName]` to `../+types/[routeName]` or any other relative path**
424 | - **After creating new routes, remind the user to run `npm run typecheck`**
425 | - **Assume types need to be generated if they're missing, don't assume the dev server is running**
426 |
--------------------------------------------------------------------------------