├── 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 |
231 | 232 | 233 | 234 |
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 |
373 | 374 | 375 |
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 | --------------------------------------------------------------------------------