diff --git a/biome.json b/biome.json index e31c7e966a..ac1b9b5caf 100644 --- a/biome.json +++ b/biome.json @@ -23,7 +23,12 @@ "*.svelte", "template-lit-*/src/my-element.*", "tsconfig.json", - "tsconfig.*.json" + "tsconfig.*.json", + "**/dist/**", + "**/public/js/**", + "**/build/**", + "**/.cache/**", + "**/.temp/**" ], "ignoreUnknown": true }, diff --git a/cspell.config.cjs b/cspell.config.cjs index 7fc6fa0afc..99533e6d0e 100644 --- a/cspell.config.cjs +++ b/cspell.config.cjs @@ -18,6 +18,7 @@ module.exports = { 'node_modules', 'pnpm-lock.yaml', 'README.pt-BR.md', + '**/public/js/**', ], flagWords: banWords, dictionaries: ['dictionary'], diff --git a/examples/react-router/cloudflare/.gitignore b/examples/react-router/cloudflare/.gitignore new file mode 100644 index 0000000000..0c402dead2 --- /dev/null +++ b/examples/react-router/cloudflare/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +/node_modules/ +*.tsbuildinfo + +# React Router +/.react-router/ +/build/ + +# Cloudflare +.mf +.wrangler diff --git a/examples/react-router/cloudflare/README.md b/examples/react-router/cloudflare/README.md new file mode 100644 index 0000000000..6a3108105f --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/app/app.css b/examples/react-router/cloudflare/app/app.css new file mode 100644 index 0000000000..ebf4604d96 --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/app/entry.server.tsx b/examples/react-router/cloudflare/app/entry.server.tsx new file mode 100644 index 0000000000..969f191d8b --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/app/root.tsx b/examples/react-router/cloudflare/app/root.tsx new file mode 100644 index 0000000000..ac3abcc678 --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/app/routes.ts b/examples/react-router/cloudflare/app/routes.ts new file mode 100644 index 0000000000..205ff3ccb9 --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/app/routes/home.tsx b/examples/react-router/cloudflare/app/routes/home.tsx new file mode 100644 index 0000000000..ef745f9590 --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/app/welcome/logo-dark.svg b/examples/react-router/cloudflare/app/welcome/logo-dark.svg new file mode 100644 index 0000000000..dd82028944 --- /dev/null +++ b/examples/react-router/cloudflare/app/welcome/logo-dark.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/react-router/cloudflare/app/welcome/logo-light.svg b/examples/react-router/cloudflare/app/welcome/logo-light.svg new file mode 100644 index 0000000000..73284929d3 --- /dev/null +++ b/examples/react-router/cloudflare/app/welcome/logo-light.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/react-router/cloudflare/app/welcome/welcome.tsx b/examples/react-router/cloudflare/app/welcome/welcome.tsx new file mode 100644 index 0000000000..38966c22d0 --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/package.json b/examples/react-router/cloudflare/package.json new file mode 100644 index 0000000000..78ebd076e2 --- /dev/null +++ b/examples/react-router/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": { + "@tailwindcss/postcss": "^4.0.0", + "@react-router/dev": "^7.1.3", + "@react-router/cloudflare": "^7.1.3", + "@cloudflare/workers-types": "^4.20241112.0", + "@rsbuild/core": "workspace:*", + "@rsbuild/plugin-react": "workspace:*", + "@rsbuild/plugin-react-router": "workspace:*", + "@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/react-router/cloudflare/postcss.config.cjs b/examples/react-router/cloudflare/postcss.config.cjs new file mode 100644 index 0000000000..e5640725a9 --- /dev/null +++ b/examples/react-router/cloudflare/postcss.config.cjs @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; diff --git a/examples/react-router/cloudflare/public/favicon.ico b/examples/react-router/cloudflare/public/favicon.ico new file mode 100644 index 0000000000..5dbdfcddcb Binary files /dev/null and b/examples/react-router/cloudflare/public/favicon.ico differ diff --git a/examples/react-router/cloudflare/react-router.config.ts b/examples/react-router/cloudflare/react-router.config.ts new file mode 100644 index 0000000000..4f9a6ed522 --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/rsbuild.config.ts b/examples/react-router/cloudflare/rsbuild.config.ts new file mode 100644 index 0000000000..745dc3e224 --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/server/app.ts b/examples/react-router/cloudflare/server/app.ts new file mode 100644 index 0000000000..a9daa9d0f7 --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/tailwind.config.ts b/examples/react-router/cloudflare/tailwind.config.ts new file mode 100644 index 0000000000..65cbafc2f7 --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/tsconfig.cloudflare.json b/examples/react-router/cloudflare/tsconfig.cloudflare.json new file mode 100644 index 0000000000..31374c2d40 --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/tsconfig.json b/examples/react-router/cloudflare/tsconfig.json new file mode 100644 index 0000000000..d7ce9e49b0 --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/tsconfig.node.json b/examples/react-router/cloudflare/tsconfig.node.json new file mode 100644 index 0000000000..8e3f1d3472 --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/worker-configuration.d.ts b/examples/react-router/cloudflare/worker-configuration.d.ts new file mode 100644 index 0000000000..3e55622e0b --- /dev/null +++ b/examples/react-router/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/react-router/cloudflare/wrangler.toml b/examples/react-router/cloudflare/wrangler.toml new file mode 100644 index 0000000000..dc2549eb6d --- /dev/null +++ b/examples/react-router/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/react-router/custom-node-server/.gitignore b/examples/react-router/custom-node-server/.gitignore new file mode 100644 index 0000000000..6c31a3918f --- /dev/null +++ b/examples/react-router/custom-node-server/.gitignore @@ -0,0 +1,4 @@ +.react-router +build +node_modules +.idea diff --git a/examples/react-router/custom-node-server/app/app.css b/examples/react-router/custom-node-server/app/app.css new file mode 100644 index 0000000000..718406f1e2 --- /dev/null +++ b/examples/react-router/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/react-router/custom-node-server/app/components/welcome.tsx b/examples/react-router/custom-node-server/app/components/welcome.tsx new file mode 100644 index 0000000000..f35ecf3288 --- /dev/null +++ b/examples/react-router/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/react-router/custom-node-server/app/entry.client.tsx b/examples/react-router/custom-node-server/app/entry.client.tsx new file mode 100644 index 0000000000..33cb007f53 --- /dev/null +++ b/examples/react-router/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/react-router/custom-node-server/app/entry.server.tsx b/examples/react-router/custom-node-server/app/entry.server.tsx new file mode 100644 index 0000000000..53a057169b --- /dev/null +++ b/examples/react-router/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/react-router/custom-node-server/app/root.tsx b/examples/react-router/custom-node-server/app/root.tsx new file mode 100644 index 0000000000..1da66379fb --- /dev/null +++ b/examples/react-router/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/react-router/custom-node-server/app/routes.ts b/examples/react-router/custom-node-server/app/routes.ts new file mode 100644 index 0000000000..d707d29eb7 --- /dev/null +++ b/examples/react-router/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/react-router/custom-node-server/app/routes/about.css b/examples/react-router/custom-node-server/app/routes/about.css new file mode 100644 index 0000000000..42dd8fc53d --- /dev/null +++ b/examples/react-router/custom-node-server/app/routes/about.css @@ -0,0 +1,3 @@ +body { + background: gray; +} diff --git a/examples/react-router/custom-node-server/app/routes/about.tsx b/examples/react-router/custom-node-server/app/routes/about.tsx new file mode 100644 index 0000000000..ae706ddcdb --- /dev/null +++ b/examples/react-router/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/react-router/custom-node-server/app/routes/docs/advanced.tsx b/examples/react-router/custom-node-server/app/routes/docs/advanced.tsx new file mode 100644 index 0000000000..fe1665ed81 --- /dev/null +++ b/examples/react-router/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/react-router/custom-node-server/app/routes/docs/getting-started.tsx b/examples/react-router/custom-node-server/app/routes/docs/getting-started.tsx new file mode 100644 index 0000000000..0d007d692a --- /dev/null +++ b/examples/react-router/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/react-router/custom-node-server/app/routes/docs/index.tsx b/examples/react-router/custom-node-server/app/routes/docs/index.tsx new file mode 100644 index 0000000000..91337f2d63 --- /dev/null +++ b/examples/react-router/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/react-router/custom-node-server/app/routes/docs/layout.tsx b/examples/react-router/custom-node-server/app/routes/docs/layout.tsx new file mode 100644 index 0000000000..dd6ded7842 --- /dev/null +++ b/examples/react-router/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/react-router/custom-node-server/app/routes/home.tsx b/examples/react-router/custom-node-server/app/routes/home.tsx new file mode 100644 index 0000000000..48a0a9bbef --- /dev/null +++ b/examples/react-router/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/react-router/custom-node-server/app/routes/projects/edit.tsx b/examples/react-router/custom-node-server/app/routes/projects/edit.tsx new file mode 100644 index 0000000000..2b2927bac3 --- /dev/null +++ b/examples/react-router/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} +

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