From a1d0591e1483a54e0e007c716983232375bdb34e Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 4 Feb 2025 12:53:33 -0800 Subject: [PATCH 01/20] chore: add examples --- .gitignore | 5 + LICENSE | 21 + config/package.json | 11 + config/rslib.config.ts | 67 + config/tsconfig-node16.json | 7 + config/tsconfig.json | 23 + examples/cloudflare/.gitignore | 11 + examples/cloudflare/README.md | 71 + examples/cloudflare/app/app.css | 15 + examples/cloudflare/app/entry.server.tsx | 44 + examples/cloudflare/app/root.tsx | 75 + examples/cloudflare/app/routes.ts | 3 + examples/cloudflare/app/routes/home.tsx | 17 + examples/cloudflare/app/welcome/logo-dark.svg | 23 + .../cloudflare/app/welcome/logo-light.svg | 23 + examples/cloudflare/app/welcome/welcome.tsx | 90 + examples/cloudflare/package.json | 35 + examples/cloudflare/postcss.config.cjs | 5 + examples/cloudflare/public/favicon.ico | Bin 0 -> 15086 bytes examples/cloudflare/react-router.config.ts | 7 + examples/cloudflare/rsbuild.config.ts | 41 + examples/cloudflare/server/app.ts | 31 + examples/cloudflare/tailwind.config.ts | 22 + examples/cloudflare/tsconfig.cloudflare.json | 28 + examples/cloudflare/tsconfig.json | 14 + examples/cloudflare/tsconfig.node.json | 13 + examples/cloudflare/worker-configuration.d.ts | 5 + examples/cloudflare/wrangler.toml | 8 + examples/custom-node-server/.gitignore | 4 + examples/custom-node-server/app/app.css | 28 + .../app/components/welcome.tsx | 130 + .../custom-node-server/app/entry.client.tsx | 12 + .../custom-node-server/app/entry.server.tsx | 71 + examples/custom-node-server/app/root.tsx | 192 + examples/custom-node-server/app/routes.ts | 32 + .../custom-node-server/app/routes/about.css | 3 + .../custom-node-server/app/routes/about.tsx | 72 + .../app/routes/docs/advanced.tsx | 123 + .../app/routes/docs/getting-started.tsx | 108 + .../app/routes/docs/index.tsx | 84 + .../app/routes/docs/layout.tsx | 48 + .../custom-node-server/app/routes/home.tsx | 99 + .../app/routes/projects/edit.tsx | 118 + .../app/routes/projects/index.tsx | 162 + .../app/routes/projects/layout.tsx | 81 + .../app/routes/projects/project.tsx | 181 + .../app/routes/projects/settings.tsx | 176 + examples/custom-node-server/package.json | 37 + .../custom-node-server/postcss.config.cjs | 5 + .../custom-node-server/react-router.config.ts | 14 + examples/custom-node-server/rsbuild.config.ts | 18 + examples/custom-node-server/server.js | 29 + examples/custom-node-server/server/app.ts | 18 + .../custom-node-server/tailwind.config.ts | 22 + examples/custom-node-server/tsconfig.json | 26 + examples/default-template/.dockerignore | 4 + examples/default-template/.gitignore | 6 + examples/default-template/Dockerfile | 22 + examples/default-template/README.md | 100 + examples/default-template/app/app.css | 15 + .../default-template/app/entry.client.tsx | 12 + .../default-template/app/entry.server.tsx | 71 + examples/default-template/app/root.tsx | 75 + examples/default-template/app/routes.ts | 3 + examples/default-template/app/routes/home.tsx | 13 + .../app/welcome/logo-dark.svg | 23 + .../app/welcome/logo-light.svg | 23 + .../default-template/app/welcome/welcome.tsx | 89 + examples/default-template/package.json | 35 + examples/default-template/postcss.config.cjs | 5 + examples/default-template/public/favicon.ico | Bin 0 -> 15086 bytes .../default-template/react-router.config.ts | 7 + examples/default-template/rsbuild.config.ts | 16 + examples/default-template/tailwind.config.ts | 22 + examples/default-template/tsconfig.json | 27 + package.json | 86 + pnpm-lock.yaml | 8469 +++++++++++++++++ pnpm-workspace.yaml | 4 + rslib.config.ts | 30 + src/babel.ts | 15 + src/dev-server.ts | 28 + src/index.ts | 708 ++ src/plugin-utils.ts | 265 + src/templates/context.ts | 12 + src/templates/entry.client.tsx | 12 + src/templates/entry.server.tsx | 71 + tests/__snapshots__/features.test.ts.snap | 52 + tests/__snapshots__/index.test.ts.snap | 152 + tests/features.test.ts | 62 + tests/index.test.ts | 153 + tsconfig.json | 11 + tsconfig.tsbuildinfo | 1 + 92 files changed, 13307 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 config/package.json create mode 100644 config/rslib.config.ts create mode 100644 config/tsconfig-node16.json create mode 100644 config/tsconfig.json create mode 100644 examples/cloudflare/.gitignore create mode 100644 examples/cloudflare/README.md create mode 100644 examples/cloudflare/app/app.css create mode 100644 examples/cloudflare/app/entry.server.tsx create mode 100644 examples/cloudflare/app/root.tsx create mode 100644 examples/cloudflare/app/routes.ts create mode 100644 examples/cloudflare/app/routes/home.tsx create mode 100644 examples/cloudflare/app/welcome/logo-dark.svg create mode 100644 examples/cloudflare/app/welcome/logo-light.svg create mode 100644 examples/cloudflare/app/welcome/welcome.tsx create mode 100644 examples/cloudflare/package.json create mode 100644 examples/cloudflare/postcss.config.cjs create mode 100644 examples/cloudflare/public/favicon.ico create mode 100644 examples/cloudflare/react-router.config.ts create mode 100644 examples/cloudflare/rsbuild.config.ts create mode 100644 examples/cloudflare/server/app.ts create mode 100644 examples/cloudflare/tailwind.config.ts create mode 100644 examples/cloudflare/tsconfig.cloudflare.json create mode 100644 examples/cloudflare/tsconfig.json create mode 100644 examples/cloudflare/tsconfig.node.json create mode 100644 examples/cloudflare/worker-configuration.d.ts create mode 100644 examples/cloudflare/wrangler.toml create mode 100644 examples/custom-node-server/.gitignore create mode 100644 examples/custom-node-server/app/app.css create mode 100644 examples/custom-node-server/app/components/welcome.tsx create mode 100644 examples/custom-node-server/app/entry.client.tsx create mode 100644 examples/custom-node-server/app/entry.server.tsx create mode 100644 examples/custom-node-server/app/root.tsx create mode 100644 examples/custom-node-server/app/routes.ts create mode 100644 examples/custom-node-server/app/routes/about.css create mode 100644 examples/custom-node-server/app/routes/about.tsx create mode 100644 examples/custom-node-server/app/routes/docs/advanced.tsx create mode 100644 examples/custom-node-server/app/routes/docs/getting-started.tsx create mode 100644 examples/custom-node-server/app/routes/docs/index.tsx create mode 100644 examples/custom-node-server/app/routes/docs/layout.tsx create mode 100644 examples/custom-node-server/app/routes/home.tsx create mode 100644 examples/custom-node-server/app/routes/projects/edit.tsx create mode 100644 examples/custom-node-server/app/routes/projects/index.tsx create mode 100644 examples/custom-node-server/app/routes/projects/layout.tsx create mode 100644 examples/custom-node-server/app/routes/projects/project.tsx create mode 100644 examples/custom-node-server/app/routes/projects/settings.tsx create mode 100644 examples/custom-node-server/package.json create mode 100644 examples/custom-node-server/postcss.config.cjs create mode 100644 examples/custom-node-server/react-router.config.ts create mode 100644 examples/custom-node-server/rsbuild.config.ts create mode 100644 examples/custom-node-server/server.js create mode 100644 examples/custom-node-server/server/app.ts create mode 100644 examples/custom-node-server/tailwind.config.ts create mode 100644 examples/custom-node-server/tsconfig.json create mode 100644 examples/default-template/.dockerignore create mode 100644 examples/default-template/.gitignore create mode 100644 examples/default-template/Dockerfile create mode 100644 examples/default-template/README.md create mode 100644 examples/default-template/app/app.css create mode 100644 examples/default-template/app/entry.client.tsx create mode 100644 examples/default-template/app/entry.server.tsx create mode 100644 examples/default-template/app/root.tsx create mode 100644 examples/default-template/app/routes.ts create mode 100644 examples/default-template/app/routes/home.tsx create mode 100644 examples/default-template/app/welcome/logo-dark.svg create mode 100644 examples/default-template/app/welcome/logo-light.svg create mode 100644 examples/default-template/app/welcome/welcome.tsx create mode 100644 examples/default-template/package.json create mode 100644 examples/default-template/postcss.config.cjs create mode 100644 examples/default-template/public/favicon.ico create mode 100644 examples/default-template/react-router.config.ts create mode 100644 examples/default-template/rsbuild.config.ts create mode 100644 examples/default-template/tailwind.config.ts create mode 100644 examples/default-template/tsconfig.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 rslib.config.ts create mode 100644 src/babel.ts create mode 100644 src/dev-server.ts create mode 100644 src/index.ts create mode 100644 src/plugin-utils.ts create mode 100644 src/templates/context.ts create mode 100644 src/templates/entry.client.tsx create mode 100644 src/templates/entry.server.tsx create mode 100644 tests/__snapshots__/features.test.ts.snap create mode 100644 tests/__snapshots__/index.test.ts.snap create mode 100644 tests/features.test.ts create mode 100644 tests/index.test.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.tsbuildinfo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d816220 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea +node_modules +*.tsbuildinfo +dist +.npmrc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..82d38c2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-present Bytedance, Inc. and its affiliates. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/config/package.json b/config/package.json new file mode 100644 index 0000000..3210ccd --- /dev/null +++ b/config/package.json @@ -0,0 +1,11 @@ +{ + "name": "@rsbuild/config", + "version": "1.0.1", + "private": true, + "devDependencies": { + "@rsbuild/core": "1.1.0", + "@rslib/core": "0.1.3", + "@types/node": "^22.10.1", + "typescript": "^5.7.2" + } +} diff --git a/config/rslib.config.ts b/config/rslib.config.ts new file mode 100644 index 0000000..ab7288d --- /dev/null +++ b/config/rslib.config.ts @@ -0,0 +1,67 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import type { Minify, RsbuildPlugin } from '@rsbuild/core'; +import { type LibConfig, defineConfig } from '@rslib/core'; + +export const commonExternals: Array = [ + 'webpack', + /[\\/]compiled[\\/]/, +]; + +export const nodeMinifyConfig: Minify = { + js: true, + css: false, + jsOptions: { + minimizerOptions: { + // preserve variable name and disable minify for easier debugging + mangle: false, + minify: false, + compress: false, + }, + }, +}; + +// Clean tsc cache to ensure the dts files can be generated correctly +export const pluginCleanTscCache: RsbuildPlugin = { + name: 'plugin-clean-tsc-cache', + setup(api) { + api.onBeforeBuild(() => { + const tsbuildinfo = path.join( + api.context.rootPath, + 'tsconfig.tsbuildinfo', + ); + if (fs.existsSync(tsbuildinfo)) { + fs.rmSync(tsbuildinfo); + } + }); + }, +}; + +export const esmConfig: LibConfig = { + format: 'esm', + syntax: 'es2021', + dts: { + build: true, + }, + plugins: [pluginCleanTscCache], + output: { + minify: nodeMinifyConfig, + }, +}; + +export const cjsConfig: LibConfig = { + format: 'cjs', + syntax: 'es2021', + output: { + minify: nodeMinifyConfig, + }, +}; + +export const dualPackage = defineConfig({ + lib: [esmConfig, cjsConfig], + tools: { + rspack: { + externals: commonExternals, + }, + }, +}); diff --git a/config/tsconfig-node16.json b/config/tsconfig-node16.json new file mode 100644 index 0000000..1cb28f6 --- /dev/null +++ b/config/tsconfig-node16.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "Node16", + "moduleResolution": "Node16" + } +} diff --git a/config/tsconfig.json b/config/tsconfig.json new file mode 100644 index 0000000..037cfff --- /dev/null +++ b/config/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "jsx": "preserve", + "target": "ES2021", + "skipLibCheck": true, + "useDefineForClassFields": true, + + /* modules */ + "module": "ES2020", + "esModuleInterop": true, + "isolatedModules": true, + "moduleResolution": "Bundler", + "resolveJsonModule": true, + + /* type checking */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Base" +} diff --git a/examples/cloudflare/.gitignore b/examples/cloudflare/.gitignore new file mode 100644 index 0000000..0c402de --- /dev/null +++ b/examples/cloudflare/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +/node_modules/ +*.tsbuildinfo + +# React Router +/.react-router/ +/build/ + +# Cloudflare +.mf +.wrangler diff --git a/examples/cloudflare/README.md b/examples/cloudflare/README.md new file mode 100644 index 0000000..6a31081 --- /dev/null +++ b/examples/cloudflare/README.md @@ -0,0 +1,71 @@ +# Welcome to React Router! + +A modern, production-ready template for building full-stack React applications using React Router. + +## Features + +- 🚀 Server-side rendering +- ⚡️ Hot Module Replacement (HMR) +- 📦 Asset bundling and optimization +- 🔄 Data loading and mutations +- 🔒 TypeScript by default +- 🎉 TailwindCSS for styling +- 📖 [React Router docs](https://reactrouter.com/) + +## Getting Started + +### Installation + +Install the dependencies: + +```bash +npm install +``` + +### Development + +Start the development server with HMR: + +```bash +npm run dev +``` + +Your application will be available at `http://localhost:5173`. + +## Building for Production + +Create a production build: + +```bash +npm run build +``` + +## Deployment + +Deployment is done using the Wrangler CLI. + +To build and deploy directly to production: + +```sh +npm run deploy +``` + +To deploy a preview URL: + +```sh +npx wrangler versions upload +``` + +You can then promote a version to production after verification or roll it out progressively. + +```sh +npx wrangler versions deploy +``` + +## Styling + +This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. + +--- + +Built with ❤️ using React Router. diff --git a/examples/cloudflare/app/app.css b/examples/cloudflare/app/app.css new file mode 100644 index 0000000..ebf4604 --- /dev/null +++ b/examples/cloudflare/app/app.css @@ -0,0 +1,15 @@ +@import 'tailwindcss'; + +@theme { + --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; +} + +html, +body { + @apply bg-white dark:bg-gray-950; + + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} diff --git a/examples/cloudflare/app/entry.server.tsx b/examples/cloudflare/app/entry.server.tsx new file mode 100644 index 0000000..969f191 --- /dev/null +++ b/examples/cloudflare/app/entry.server.tsx @@ -0,0 +1,44 @@ +import { isbot } from 'isbot'; +import { renderToReadableStream } from 'react-dom/server'; +import type { AppLoadContext, EntryContext } from 'react-router'; +import { ServerRouter } from 'react-router'; + +export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + _loadContext: AppLoadContext, +) { + let shellRendered = false; + const userAgent = request.headers.get('user-agent'); + + const body = await renderToReadableStream( + , + { + onError(error: unknown) { + // biome-ignore lint: intentional parameter reassignment + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }, + ); + shellRendered = true; + + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) { + await body.allReady; + } + + responseHeaders.set('Content-Type', 'text/html'); + return new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }); +} diff --git a/examples/cloudflare/app/root.tsx b/examples/cloudflare/app/root.tsx new file mode 100644 index 0000000..ac3abcc --- /dev/null +++ b/examples/cloudflare/app/root.tsx @@ -0,0 +1,75 @@ +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + isRouteErrorResponse, +} from 'react-router'; + +import type { Route } from './+types/root'; +import './app.css'; + +export const links: Route.LinksFunction = () => [ + { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, + { + rel: 'preconnect', + href: 'https://fonts.gstatic.com', + crossOrigin: 'anonymous', + }, + { + rel: 'stylesheet', + href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap', + }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + let message = 'Oops!'; + let details = 'An unexpected error occurred.'; + let stack: string | undefined; + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? '404' : 'Error'; + details = + error.status === 404 + ? 'The requested page could not be found.' + : error.statusText || details; + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message; + stack = error.stack; + } + + return ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ); +} diff --git a/examples/cloudflare/app/routes.ts b/examples/cloudflare/app/routes.ts new file mode 100644 index 0000000..205ff3c --- /dev/null +++ b/examples/cloudflare/app/routes.ts @@ -0,0 +1,3 @@ +import { type RouteConfig, index } from '@react-router/dev/routes'; + +export default [index('routes/home.tsx')] satisfies RouteConfig; diff --git a/examples/cloudflare/app/routes/home.tsx b/examples/cloudflare/app/routes/home.tsx new file mode 100644 index 0000000..ef745f9 --- /dev/null +++ b/examples/cloudflare/app/routes/home.tsx @@ -0,0 +1,17 @@ +import { Welcome } from '../welcome/welcome'; +import type { Route } from './+types/home'; + +export function meta({}: Route.MetaArgs) { + return [ + { title: 'New React Router App' }, + { name: 'description', content: 'Welcome to React Router!' }, + ]; +} + +export function loader({ context }: Route.LoaderArgs) { + return { message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE }; +} + +export default function Home({ loaderData }: Route.ComponentProps) { + return ; +} diff --git a/examples/cloudflare/app/welcome/logo-dark.svg b/examples/cloudflare/app/welcome/logo-dark.svg new file mode 100644 index 0000000..dd82028 --- /dev/null +++ b/examples/cloudflare/app/welcome/logo-dark.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/cloudflare/app/welcome/logo-light.svg b/examples/cloudflare/app/welcome/logo-light.svg new file mode 100644 index 0000000..7328492 --- /dev/null +++ b/examples/cloudflare/app/welcome/logo-light.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/cloudflare/app/welcome/welcome.tsx b/examples/cloudflare/app/welcome/welcome.tsx new file mode 100644 index 0000000..38966c2 --- /dev/null +++ b/examples/cloudflare/app/welcome/welcome.tsx @@ -0,0 +1,90 @@ +import logoDark from './logo-dark.svg'; +import logoLight from './logo-light.svg'; + +export function Welcome({ message }: { message: string }) { + return ( +
+
+
+
+ React Router + React Router +
+
+
+ +
+
+
+ ); +} + +const resources = [ + { + href: 'https://reactrouter.com/docs', + text: 'React Router Docs', + icon: ( + + + + ), + }, + { + href: 'https://rmx.as/discord', + text: 'Join Discord', + icon: ( + + + + ), + }, +]; diff --git a/examples/cloudflare/package.json b/examples/cloudflare/package.json new file mode 100644 index 0000000..8a2ad16 --- /dev/null +++ b/examples/cloudflare/package.json @@ -0,0 +1,35 @@ +{ + "name": "cloudflare", + "private": true, + "type": "module", + "scripts": { + "build": "rsbuild build", + "deploy": "npm run build && wrangler deploy", + "dev": "rsbuild dev", + "start": "wrangler dev", + "typecheck": "tsc -b" + }, + "dependencies": { + "@react-router/node": "^7.1.3", + "@react-router/serve": "^7.1.3", + "isbot": "^5.1.17", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router": "^7.1.3" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20241112.0", + "@react-router/cloudflare": "^7.1.3", + "@react-router/dev": "^7.1.3", + "@rsbuild/core": "^1.2.3", + "@rsbuild/plugin-react": "^1.1.0", + "@rsbuild/plugin-react-router": "workspace:*", + "@tailwindcss/postcss": "^4.0.0", + "@types/node": "^20", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.1", + "tailwindcss": "^4.0.0", + "typescript": "^5.7.2", + "wrangler": "^3.106.0" + } +} diff --git a/examples/cloudflare/postcss.config.cjs b/examples/cloudflare/postcss.config.cjs new file mode 100644 index 0000000..e564072 --- /dev/null +++ b/examples/cloudflare/postcss.config.cjs @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; diff --git a/examples/cloudflare/public/favicon.ico b/examples/cloudflare/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5dbdfcddcb14182535f6d32d1c900681321b1aa3 GIT binary patch literal 15086 zcmeI33v3ic7{|AFEmuJ-;v>ep_G*NPi6KM`qNryCe1PIJ8siIN1WZ(7qVa)RVtmC% z)Ch?tN+afMKm;5@rvorJk zcXnoOc4q51HBQnQH_jn!cAg&XI1?PlX>Kl^k8qq0;zkha`kY$Fxt#=KNJAE9CMdpW zqr4#g8`nTw191(+H4xW8Tmyru2I^3=J1G3emPxkPXA=3{vvuvse_WWSshqaqls^-m zgB7q8&Vk*aYRe?sn$n53dGH#%3y%^vxv{pL*-h0Z4bmb_(k6{FL7HWIz(V*HT#IcS z-wE{)+0x1U!RUPt3gB97%p}@oHxF4|6S*+Yw=_tLtxZ~`S=z6J?O^AfU>7qOX`JNBbV&8+bO0%@fhQitKIJ^O^ zpgIa__qD_y07t@DFlBJ)8SP_#^j{6jpaXt{U%=dx!qu=4u7^21lWEYHPPY5U3TcoQ zX_7W+lvZi>TapNk_X>k-KO%MC9iZp>1E`N34gHKd9tK&){jq2~7OsJ>!G0FzxQFw6G zm&Vb(2#-T|rM|n3>uAsG_hnbvUKFf3#ay@u4uTzia~NY%XgCHfx4^To4BDU@)HlV? z@EN=g^ymETa1sQK{kRwyE4Ax8?wT&GvaG@ASO}{&a17&^v`y z!oPdiSiia^oov(Z)QhG2&|FgE{M9_4hJROGbnj>#$~ZF$-G^|zPj*QApltKe?;u;uKHJ~-V!=VLkg7Kgct)l7u39f@%VG8e3f$N-B zAu3a4%ZGf)r+jPAYCSLt73m_J3}p>}6Tx0j(wg4vvKhP!DzgiWANiE;Ppvp}P2W@m z-VbYn+NXFF?6ngef5CfY6ZwKnWvNV4z6s^~yMXw2i5mv}jC$6$46g?G|CPAu{W5qF zDobS=zb2ILX9D827g*NtGe5w;>frjanY{f)hrBP_2ehBt1?`~ypvg_Ot4x1V+43P@Ve8>qd)9NX_jWdLo`Zfy zoeam9)@Dpym{4m@+LNxXBPjPKA7{3a&H+~xQvr>C_A;7=JrfK~$M2pCh>|xLz>W6SCs4qC|#V`)# z)0C|?$o>jzh<|-cpf

K7osU{Xp5PG4-K+L2G=)c3f&}H&M3wo7TlO_UJjQ-Oq&_ zjAc9=nNIYz{c3zxOiS5UfcE1}8#iI4@uy;$Q7>}u`j+OU0N<*Ezx$k{x_27+{s2Eg z`^=rhtIzCm!_UcJ?Db~Lh-=_))PT3{Q0{Mwdq;0>ZL%l3+;B&4!&xm#%HYAK|;b456Iv&&f$VQHf` z>$*K9w8T+paVwc7fLfMlhQ4)*zL_SG{~v4QR;IuX-(oRtYAhWOlh`NLoX0k$RUYMi z2Y!bqpdN}wz8q`-%>&Le@q|jFw92ErW-hma-le?S z-@OZt2EEUm4wLsuEMkt4zlyy29_3S50JAcQHTtgTC{P~%-mvCTzrjXOc|{}N`Cz`W zSj7CrXfa7lcsU0J(0uSX6G`54t^7}+OLM0n(|g4waOQ}bd3%!XLh?NX9|8G_|06Ie zD5F1)w5I~!et7lA{G^;uf7aqT`KE&2qx9|~O;s6t!gb`+zVLJyT2T)l*8l(j literal 0 HcmV?d00001 diff --git a/examples/cloudflare/react-router.config.ts b/examples/cloudflare/react-router.config.ts new file mode 100644 index 0000000..4f9a6ed --- /dev/null +++ b/examples/cloudflare/react-router.config.ts @@ -0,0 +1,7 @@ +import type { Config } from '@react-router/dev/config'; + +export default { + // Config options... + // Server-side render by default, to enable SPA mode set this to `false` + ssr: true, +} satisfies Config; diff --git a/examples/cloudflare/rsbuild.config.ts b/examples/cloudflare/rsbuild.config.ts new file mode 100644 index 0000000..745dc3e --- /dev/null +++ b/examples/cloudflare/rsbuild.config.ts @@ -0,0 +1,41 @@ +import { defineConfig } from '@rsbuild/core'; +import { pluginReact } from '@rsbuild/plugin-react'; +import { pluginReactRouter } from '@rsbuild/plugin-react-router'; + +export default defineConfig({ + environments: { + node: { + performance: { + // cloudflare cannot support dynamic chunk split in worker + chunkSplit: { strategy: 'all-in-one' }, + }, + tools: { + rspack: { + // must use esm module output + experiments: { + outputModule: true, + }, + externalsType: 'module', + output: { + chunkFormat: 'module', + chunkLoading: 'import', + workerChunkLoading: 'import', + wasmLoading: 'fetch', + library: { type: 'module' }, + module: true, + }, + resolve: { + conditionNames: [ + 'workerd', + 'worker', + 'browser', + 'import', + 'require', + ], + }, + }, + }, + }, + }, + plugins: [pluginReactRouter(), pluginReact()], +}); diff --git a/examples/cloudflare/server/app.ts b/examples/cloudflare/server/app.ts new file mode 100644 index 0000000..a9daa9d --- /dev/null +++ b/examples/cloudflare/server/app.ts @@ -0,0 +1,31 @@ +import { createRequestHandler } from 'react-router'; + +declare global { + interface CloudflareEnvironment extends Env {} + interface ImportMeta { + env: { + MODE: string; + }; + } +} + +declare module 'react-router' { + export interface AppLoadContext { + cloudflare: { + env: CloudflareEnvironment; + ctx: ExecutionContext; + }; + } +} +// @ts-expect-error - virtual module provided by React Router at build time +import * as serverBuild from 'virtual/react-router/server-build'; + +const requestHandler = createRequestHandler(serverBuild, import.meta.env.MODE); + +export default { + fetch(request, env, ctx) { + return requestHandler(request, { + cloudflare: { env, ctx }, + }); + }, +} satisfies ExportedHandler; diff --git a/examples/cloudflare/tailwind.config.ts b/examples/cloudflare/tailwind.config.ts new file mode 100644 index 0000000..65cbafc --- /dev/null +++ b/examples/cloudflare/tailwind.config.ts @@ -0,0 +1,22 @@ +import type { Config } from 'tailwindcss'; + +export default { + content: ['./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}'], + theme: { + extend: { + fontFamily: { + sans: [ + '"Inter"', + 'ui-sans-serif', + 'system-ui', + 'sans-serif', + '"Apple Color Emoji"', + '"Segoe UI Emoji"', + '"Segoe UI Symbol"', + '"Noto Color Emoji"', + ], + }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/examples/cloudflare/tsconfig.cloudflare.json b/examples/cloudflare/tsconfig.cloudflare.json new file mode 100644 index 0000000..31374c2 --- /dev/null +++ b/examples/cloudflare/tsconfig.cloudflare.json @@ -0,0 +1,28 @@ +{ + "extends": "./tsconfig.json", + "include": [ + ".react-router/types/**/*", + "app/**/*", + "app/**/.server/**/*", + "app/**/.client/**/*", + "server/**/*", + "worker-configuration.d.ts" + ], + "compilerOptions": { + "composite": true, + "strict": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["@cloudflare/workers-types"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "baseUrl": ".", + "rootDirs": [".", "./.react-router/types"], + "paths": { + "~/*": ["./app/*"] + }, + "esModuleInterop": true, + "resolveJsonModule": true + } +} diff --git a/examples/cloudflare/tsconfig.json b/examples/cloudflare/tsconfig.json new file mode 100644 index 0000000..d7ce9e4 --- /dev/null +++ b/examples/cloudflare/tsconfig.json @@ -0,0 +1,14 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.cloudflare.json" } + ], + "compilerOptions": { + "checkJs": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true + } +} diff --git a/examples/cloudflare/tsconfig.node.json b/examples/cloudflare/tsconfig.node.json new file mode 100644 index 0000000..8e3f1d3 --- /dev/null +++ b/examples/cloudflare/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "include": ["tailwind.config.ts", "vite.config.ts"], + "compilerOptions": { + "composite": true, + "strict": true, + "types": ["node"], + "lib": ["ES2022"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler" + } +} diff --git a/examples/cloudflare/worker-configuration.d.ts b/examples/cloudflare/worker-configuration.d.ts new file mode 100644 index 0000000..3e55622 --- /dev/null +++ b/examples/cloudflare/worker-configuration.d.ts @@ -0,0 +1,5 @@ +// Generated by Wrangler by running `wrangler types` + +interface Env { + VALUE_FROM_CLOUDFLARE: 'Hello from Cloudflare'; +} diff --git a/examples/cloudflare/wrangler.toml b/examples/cloudflare/wrangler.toml new file mode 100644 index 0000000..dc2549e --- /dev/null +++ b/examples/cloudflare/wrangler.toml @@ -0,0 +1,8 @@ +workers_dev = true +name = "my-react-router-worker" +compatibility_date = "2024-11-18" +main = "./build/server/static/js/app.js" +assets = { directory = "./build/client/" } + +[vars] +VALUE_FROM_CLOUDFLARE = "Hello from Cloudflare" diff --git a/examples/custom-node-server/.gitignore b/examples/custom-node-server/.gitignore new file mode 100644 index 0000000..6c31a39 --- /dev/null +++ b/examples/custom-node-server/.gitignore @@ -0,0 +1,4 @@ +.react-router +build +node_modules +.idea diff --git a/examples/custom-node-server/app/app.css b/examples/custom-node-server/app/app.css new file mode 100644 index 0000000..718406f --- /dev/null +++ b/examples/custom-node-server/app/app.css @@ -0,0 +1,28 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body { + @apply bg-white dark:bg-gray-950 min-h-screen font-sans; + + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} + +.nav-link { + @apply px-4 py-2 rounded-lg transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-800; +} + +.nav-link.active { + @apply bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-100; +} + +.page-container { + @apply max-w-7xl mx-auto px-4 py-8; +} + +.card { + @apply bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6; +} diff --git a/examples/custom-node-server/app/components/welcome.tsx b/examples/custom-node-server/app/components/welcome.tsx new file mode 100644 index 0000000..f35ecf3 --- /dev/null +++ b/examples/custom-node-server/app/components/welcome.tsx @@ -0,0 +1,130 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource react */ + +import { Link } from 'react-router'; + +// import logoDark from "./logo-dark.svg"; +// import logoLight from "./logo-light.svg"; + +const resources = [ + { + href: 'https://reactrouter.com/docs', + text: 'React Router Documentation', + description: 'Learn everything about React Router v6 and its features.', + icon: ( + + ), + }, + { + href: 'https://github.com/remix-run/react-router', + text: 'GitHub Repository', + description: 'Explore the source code and contribute to React Router.', + icon: ( + + ), + }, + { + href: 'https://reactrouter.com/blog', + text: 'React Router Blog', + description: 'Stay updated with the latest news and updates.', + icon: ( + + ), + }, +]; + +export function Welcome({ message }: { message: string }) { + return ( +

+
+

+ {message} +

+

+ Get started with React Router and explore its powerful features +

+
+ +
+ {resources.map(({ href, text, description, icon }) => ( + +
+ {icon} +

+ {text} +

+
+

{description}

+
+ ))} +
+ +
+

+ Ready to explore more? +

+

+ Check out our about page to learn more about the technologies used in + this demo. +

+ + View About Page + +
+
+ ); +} diff --git a/examples/custom-node-server/app/entry.client.tsx b/examples/custom-node-server/app/entry.client.tsx new file mode 100644 index 0000000..33cb007 --- /dev/null +++ b/examples/custom-node-server/app/entry.client.tsx @@ -0,0 +1,12 @@ +import { StrictMode, startTransition } from 'react'; +import { hydrateRoot } from 'react-dom/client'; +import { HydratedRouter } from 'react-router/dom'; + +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); diff --git a/examples/custom-node-server/app/entry.server.tsx b/examples/custom-node-server/app/entry.server.tsx new file mode 100644 index 0000000..53a0571 --- /dev/null +++ b/examples/custom-node-server/app/entry.server.tsx @@ -0,0 +1,71 @@ +import { PassThrough } from 'node:stream'; + +import { createReadableStreamFromReadable } from '@react-router/node'; +import { isbot } from 'isbot'; +import type { RenderToPipeableStreamOptions } from 'react-dom/server'; +import { renderToPipeableStream } from 'react-dom/server'; +import type { AppLoadContext, EntryContext } from 'react-router'; +import { ServerRouter } from 'react-router'; + +export const streamTimeout = 5_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + loadContext: AppLoadContext, +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const userAgent = request.headers.get('user-agent'); + + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + const readyOption: keyof RenderToPipeableStreamOptions = + (userAgent && isbot(userAgent)) || routerContext.isSpaMode + ? 'onAllReady' + : 'onShellReady'; + + let status = responseStatusCode; + const headers = new Headers(responseHeaders); + + const { pipe, abort } = renderToPipeableStream( + , + { + [readyOption]() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + headers.set('Content-Type', 'text/html'); + + resolve( + new Response(stream, { + headers, + status, + }), + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + status = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }, + ); + + // Abort the rendering stream after the `streamTimeout` so it has tine to + // flush down the rejected boundaries + setTimeout(abort, streamTimeout + 1000); + }); +} diff --git a/examples/custom-node-server/app/root.tsx b/examples/custom-node-server/app/root.tsx new file mode 100644 index 0000000..1da6637 --- /dev/null +++ b/examples/custom-node-server/app/root.tsx @@ -0,0 +1,192 @@ +import { + Link, + Links, + Meta, + NavLink, + Outlet, + Scripts, + ScrollRestoration, + isRouteErrorResponse, + useLocation, + useMatches, + useRouteError, +} from 'react-router'; + +import type { Route } from './+types/root'; +import './app.css'; +// import stylesheet from "./app.css?url"; +// console.log(stylesheet); + +interface RouteHandle { + breadcrumb?: (data: any) => string; +} + +interface RouteMatch { + id: string; + pathname: string; + params: Record; + data: any; + handle: RouteHandle; +} + +export const links: Route.LinksFunction = () => [ + { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, + { + rel: 'preconnect', + href: 'https://fonts.gstatic.com', + crossOrigin: 'anonymous', + }, + { + rel: 'stylesheet', + href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap', + }, + // { rel: "stylesheet", href: stylesheet }, +]; + +function Navigation() { + const location = useLocation(); + const matches = useMatches() as RouteMatch[]; + + const mainNavItems = [ + { to: '/', label: 'Home' }, + { to: '/about', label: 'About' }, + { to: '/docs', label: 'Documentation' }, + { to: '/projects', label: 'Projects' }, + ]; + + const breadcrumbs = matches + .filter((match) => Boolean(match.handle?.breadcrumb)) + .map((match) => ({ + to: match.pathname, + label: match.handle.breadcrumb?.(match.data) || '', + })); + + return ( +
+
+ {/* Main Navigation */} + + + {/* Breadcrumbs */} + {breadcrumbs.length > 0 && ( + + )} +
+
+ ); +} + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + +
+ +
{children}
+
+
+ React Router Demo Application +
+
+
+ + + + + ); +} + +export default function App() { + return ; +} + +// export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { +export function ErrorBoundary() { + const error = useRouteError(); + + let message = 'Oops!'; + let details = 'An unexpected error occurred.'; + let stack: string | undefined; + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? '404' : 'Error'; + details = + error.status === 404 + ? 'The requested page could not be found.' + : error.statusText || details; + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message; + stack = error.stack; + } + + return ( +
+
+

+ {message} +

+

+ {details} +

+ {stack && ( +
+            {stack}
+          
+ )} + + Return Home + +
+
+ ); +} diff --git a/examples/custom-node-server/app/routes.ts b/examples/custom-node-server/app/routes.ts new file mode 100644 index 0000000..d707d29 --- /dev/null +++ b/examples/custom-node-server/app/routes.ts @@ -0,0 +1,32 @@ +import { + type RouteConfig, + index, + layout, + prefix, + route, +} from '@react-router/dev/routes'; + +export default [ + // Index route for the home page + index('routes/home.tsx'), + + // About page + route('about', 'routes/about.tsx'), + + // Docs section with nested routes + layout('routes/docs/layout.tsx', [ + index('routes/docs/index.tsx'), + route('getting-started', 'routes/docs/getting-started.tsx'), + route('advanced', 'routes/docs/advanced.tsx'), + ]), + + // Projects section with dynamic segments + ...prefix('projects', [ + index('routes/projects/index.tsx'), + layout('routes/projects/layout.tsx', [ + route(':projectId', 'routes/projects/project.tsx'), + route(':projectId/edit', 'routes/projects/edit.tsx'), + route(':projectId/settings', 'routes/projects/settings.tsx'), + ]), + ]), +] satisfies RouteConfig; diff --git a/examples/custom-node-server/app/routes/about.css b/examples/custom-node-server/app/routes/about.css new file mode 100644 index 0000000..42dd8fc --- /dev/null +++ b/examples/custom-node-server/app/routes/about.css @@ -0,0 +1,3 @@ +body { + background: gray; +} diff --git a/examples/custom-node-server/app/routes/about.tsx b/examples/custom-node-server/app/routes/about.tsx new file mode 100644 index 0000000..ae706dd --- /dev/null +++ b/examples/custom-node-server/app/routes/about.tsx @@ -0,0 +1,72 @@ +import { Link } from 'react-router'; +import './about.css'; + +const teamMembers = [ + { + name: 'React Router', + role: 'Routing Library', + description: 'The most popular routing solution for React applications.', + link: 'https://reactrouter.com', + }, + { + name: 'Tailwind CSS', + role: 'Styling Framework', + description: 'A utility-first CSS framework for rapid UI development.', + link: 'https://tailwindcss.com', + }, + { + name: 'TypeScript', + role: 'Programming Language', + description: + 'A typed superset of JavaScript that compiles to plain JavaScript.', + link: 'https://www.typescriptlang.org', + }, +]; + +export default function About() { + return ( +
+
+

+ About This Demo +

+

+ A showcase of modern web development tools and practices +

+
+ +
+ {teamMembers.map((member) => ( +
+

+ {member.name} +

+

+ {member.role} +

+

+ {member.description} +

+ + Learn more → + +
+ ))} +
+ +
+ + ← Back to Home + +
+
+ ); +} diff --git a/examples/custom-node-server/app/routes/docs/advanced.tsx b/examples/custom-node-server/app/routes/docs/advanced.tsx new file mode 100644 index 0000000..fe1665e --- /dev/null +++ b/examples/custom-node-server/app/routes/docs/advanced.tsx @@ -0,0 +1,123 @@ +import { Link } from 'react-router'; + +export function handle() { + return { + breadcrumb: () => 'Advanced Concepts', + }; +} + +const loaderCode = `// Route definition +{ + path: "projects/:projectId", + element: , + loader: async ({ params }) => { + const project = await fetchProject(params.projectId); + if (!project) { + throw new Response("", { status: 404 }); + } + return project; + }, +} + +// Component +function Project() { + const project = useLoaderData(); + return

{project.name}

; +}`; + +const actionCode = `// Route definition +{ + path: "projects/new", + element: , + action: async ({ request }) => { + const formData = await request.formData(); + const project = await createProject(formData); + return redirect(\`/projects/\${project.id}\`); + }, +} + +// Component +function NewProject() { + const { state } = useNavigation(); + const isSubmitting = state === "submitting"; + + return ( +
+ + +
+ ); +}`; + +const errorCode = `// Route definition +{ + path: "projects/:projectId", + element: , + errorElement: , +} + +// Error component +function ProjectError() { + const error = useRouteError(); + return ( +
+

Oops!

+

{error.message}

+
+ ); +}`; + +export default function Advanced() { + return ( +
+

Advanced React Router Concepts

+

+ Explore powerful features like data loading, form handling, and error + boundaries. +

+ +

Data Loading with Loaders

+

+ Loaders let you load data before rendering a route. They run before the + route is rendered and their data is available to the component via the{' '} + useLoaderData hook: +

+
+        {loaderCode}
+      
+ +

Form Handling with Actions

+

+ Actions handle form submissions and other data mutations. They work with + the Form component to provide a seamless form handling + experience: +

+
+        {actionCode}
+      
+ +

Error Handling

+

+ Error boundaries catch errors during rendering, data loading, and data + mutations: +

+
+        {errorCode}
+      
+ +

Next Steps

+

+ Check out our{' '} + + Projects Demo + {' '} + to see these concepts in action. +

+
+ ); +} diff --git a/examples/custom-node-server/app/routes/docs/getting-started.tsx b/examples/custom-node-server/app/routes/docs/getting-started.tsx new file mode 100644 index 0000000..0d007d6 --- /dev/null +++ b/examples/custom-node-server/app/routes/docs/getting-started.tsx @@ -0,0 +1,108 @@ +import { Link } from 'react-router'; + +export function handle() { + return { + breadcrumb: () => 'Getting Started', + }; +} + +const installCode = `# Using npm +npm install react-router-dom + +# Using yarn +yarn add react-router-dom + +# Using pnpm +pnpm add react-router-dom`; + +const setupCode = `import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import Root from "./routes/root"; +import ErrorPage from "./error-page"; +import Contact from "./routes/contact"; + +const router = createBrowserRouter([ + { + path: "/", + element: , + errorElement: , + children: [ + { + path: "contacts/:contactId", + element: , + }, + ], + }, +]); + +ReactDOM.createRoot(document.getElementById("root")).render( + +);`; + +export default function GettingStarted() { + return ( +
+

Getting Started with React Router

+

+ Learn how to add React Router to your project and create your first + routes. +

+ +

Installation

+

First, install React Router using your preferred package manager:

+
+        {installCode}
+      
+ +

Basic Setup

+

+ Create a router instance and wrap your app with{' '} + RouterProvider: +

+
+        {setupCode}
+      
+ +

Creating Routes

+

Routes are defined as objects with the following properties:

+
    +
  • + path - The URL pattern for this route +
  • +
  • + element - The component to render for this route +
  • +
  • + errorElement - Component to render when an error occurs +
  • +
  • + children - Nested routes +
  • +
+ +

URL Parameters

+

+ Dynamic segments in your routes are marked with a colon, like{' '} + :contactId in the example above. Access these parameters + using the useParams hook: +

+
+        {`function Contact() {
+  const { contactId } = useParams();
+  return 

Contact {contactId}

; +}`}
+
+ +

Next Steps

+

+ Now that you understand the basics, check out the{' '} + + Advanced Concepts + {' '} + to learn about loaders, actions, and more. +

+
+ ); +} diff --git a/examples/custom-node-server/app/routes/docs/index.tsx b/examples/custom-node-server/app/routes/docs/index.tsx new file mode 100644 index 0000000..91337f2 --- /dev/null +++ b/examples/custom-node-server/app/routes/docs/index.tsx @@ -0,0 +1,84 @@ +import { Link } from 'react-router'; + +export function handle() { + return { + breadcrumb: () => 'Introduction', + }; +} + +const exampleCode = `import { createBrowserRouter } from "react-router-dom"; + +const router = createBrowserRouter([ + { + path: "/", + element: , + children: [ + { + path: "dashboard", + element: , + }, + ], + }, +]);`; + +export default function DocsIndex() { + return ( +
+

Introduction to React Router

+

+ React Router is a powerful routing library for React applications that + enables you to build single-page applications with dynamic, client-side + routing. +

+ +

Key Features

+
    +
  • + Dynamic Routes - Create routes with URL parameters + and handle them dynamically +
  • +
  • + Nested Routes - Organize your application with nested + layouts and routes +
  • +
  • + Route Protection - Implement authentication and + protect sensitive routes +
  • +
  • + Data Loading - Load data for your routes before + rendering +
  • +
+ +

Getting Started

+

+ Ready to start building? Check out our{' '} + + Getting Started + {' '} + guide to learn the basics. +

+ +

Example Usage

+
+        {exampleCode}
+      
+ +

Next Steps

+

+ Once you're comfortable with the basics, explore our{' '} + + Advanced Concepts + {' '} + to learn about more powerful features. +

+
+ ); +} diff --git a/examples/custom-node-server/app/routes/docs/layout.tsx b/examples/custom-node-server/app/routes/docs/layout.tsx new file mode 100644 index 0000000..dd6ded7 --- /dev/null +++ b/examples/custom-node-server/app/routes/docs/layout.tsx @@ -0,0 +1,48 @@ +import { Link, NavLink, Outlet } from 'react-router'; + +const sidebarItems = [ + { to: '/docs', label: 'Introduction', exact: true }, + { to: '/docs/getting-started', label: 'Getting Started' }, + { to: '/docs/advanced', label: 'Advanced Concepts' }, +]; + +export function handle() { + return { + breadcrumb: () => 'Documentation', + }; +} + +export default function DocsLayout() { + return ( +
+
+ {/* Sidebar */} + + + {/* Main Content */} +
+ +
+
+
+ ); +} diff --git a/examples/custom-node-server/app/routes/home.tsx b/examples/custom-node-server/app/routes/home.tsx new file mode 100644 index 0000000..48a0a9b --- /dev/null +++ b/examples/custom-node-server/app/routes/home.tsx @@ -0,0 +1,99 @@ +import { useState } from 'react'; +import { Link } from 'react-router'; +import { Welcome } from '../components/welcome'; +import type { Route } from './+types/home'; + +export function meta(_: Route.MetaArgs) { + return [ + { title: 'React Router Demo' }, + { name: 'description', content: 'A modern React Router demo application' }, + ]; +} + +export function loader({ context }: Route.LoaderArgs) { + return { message: 'Welcome to React Router' }; +} + +const features = [ + { + title: 'Dynamic Routing', + description: + 'React Router enables dynamic, client-side routing in your React applications.', + link: '/about', + }, + { + title: 'Nested Routes', + description: 'Organize your application with nested routes and layouts.', + link: '/about', + }, + { + title: 'Route Protection', + description: 'Implement authentication and protect your routes easily.', + link: '/about', + }, +]; + +export default function Home({ loaderData }: Route.ComponentProps) { + const [activeFeature, setActiveFeature] = useState(0); + + return ( + <> + + +
+
+ {features.map((feature, index) => ( +
setActiveFeature(index)} + > +

+ {feature.title} +

+

+ {feature.description} +

+ + Learn more → + +
+ ))} +
+ +
+

+ Ready to learn more? +

+

+ Check out our about page to learn more about the technologies used + in this demo. +

+ + View About Page + +
+
+ + ); +} + +function Counter() { + const [count, setCount] = useState(0); + return ( +
+

Count: {count}

+ +
+ ); +} diff --git a/examples/custom-node-server/app/routes/projects/edit.tsx b/examples/custom-node-server/app/routes/projects/edit.tsx new file mode 100644 index 0000000..2b2927b --- /dev/null +++ b/examples/custom-node-server/app/routes/projects/edit.tsx @@ -0,0 +1,118 @@ +import { Form, Link, useLoaderData, useNavigation } from 'react-router'; +import type { Route } from './+types/edit'; + +export function handle() { + return { + breadcrumb: (data: Route.LoaderData) => `Edit ${data.project.name}`, + }; +} + +export function loader({ params }: Route.LoaderArgs) { + // Simulated data - in a real app, this would come from a database + return { + project: { + id: params.projectId, + name: 'React Router', + description: 'A comprehensive routing library for React applications.', + status: 'active', + team: ['1', '2', '3'], + }, + }; +} + +export async function action({ request, params }: Route.ActionArgs) { + const formData = await request.formData(); + const updates = Object.fromEntries(formData); + + // Simulated update - in a real app, this would update the database + console.log('Updating project', params.projectId, updates); + + return { ok: true }; +} + +export default function EditProject() { + const { project } = useLoaderData(); + const navigation = useNavigation(); + const isSubmitting = navigation.state === 'submitting'; + + return ( +
+
+

+ Edit Project: {project.name} +

+
+ +
+
+
+ + +
+ +
+ +