Skip to content

Commit 7cd9e69

Browse files
committed
first
0 parents  commit 7cd9e69

18 files changed

+31606
-0
lines changed

Diff for: .gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/node_modules
2+
/dist

Diff for: README.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# React Router Custom Framework
2+
3+
This completely-not-production-ready "framework" example shows the touch-points you can use with React Router to build your bundling and server abstractions around it like `@react-router/dev` does.
4+
5+
[React Router Docs](https://reactrouter.com)
6+
7+
## Running the app
8+
9+
```sh
10+
pnpm i
11+
pnpm start
12+
```
13+
14+
## Goofing around with the app
15+
16+
```sh
17+
pnpm i
18+
pnpm dev
19+
```
20+
21+
## Caveats
22+
23+
I whipped this together REALLY quickly, it certainly has errors and could be more thorough, but I hope it helps!

Diff for: app/about.loader.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { data } from "react-router";
2+
3+
export default async function load() {
4+
await new Promise(resolve => setTimeout(resolve, 200));
5+
6+
let isServer = typeof document === "undefined";
7+
let env = isServer ? "server" : "client";
8+
9+
return data(
10+
{ message: `About loader from ${env} loader` },
11+
{
12+
headers: { "X-Custom": "Hello" },
13+
},
14+
);
15+
}

Diff for: app/about.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { useLoaderData } from "react-router";
2+
import type loader from "./about.loader.js";
3+
4+
export default function About() {
5+
let data = useLoaderData<typeof loader>();
6+
return <h1>{data.message}</h1>;
7+
}

Diff for: app/home.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Home() {
2+
return <h1>Home</h1>;
3+
}

Diff for: app/layout.client.tsx

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { type LoaderFunctionArgs } from "react-router";
2+
3+
export async function loader({ request }: LoaderFunctionArgs) {
4+
let url = new URL(request.url);
5+
let res = await fetch(url, {
6+
headers: {
7+
Accept: "application/json",
8+
"X-Route-Id": "layout",
9+
},
10+
});
11+
return res.json();
12+
}
13+
14+
export async function action({ request }: LoaderFunctionArgs) {
15+
let url = new URL(request.url);
16+
// call the server action
17+
let res = await fetch(url, {
18+
method: "POST",
19+
// @ts-expect-error this is valid, types are wrong
20+
body: new URLSearchParams(await request.formData()),
21+
headers: {
22+
"Content-Type": "application/x-www-form-urlencoded",
23+
Accept: "application/json",
24+
"X-Route-Id": "layout",
25+
},
26+
});
27+
return res.json();
28+
}

Diff for: app/layout.server.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ActionFunctionArgs, type LoaderFunctionArgs } from "react-router";
2+
3+
let db = { message: "Hello world!" };
4+
5+
export async function loader(args: LoaderFunctionArgs) {
6+
await new Promise(resolve => setTimeout(resolve, 200));
7+
return { message: db.message };
8+
}
9+
10+
export async function action({ request }: ActionFunctionArgs) {
11+
let formData = await request.formData();
12+
db.message = String(formData.get("message"));
13+
return { ok: true };
14+
}

Diff for: app/layout.tsx

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Form, Link, Outlet, useLoaderData } from "react-router";
2+
import { type loader } from "./layout.server.js";
3+
4+
export default function Layout() {
5+
let data = useLoaderData<typeof loader>();
6+
return (
7+
<html>
8+
<head>
9+
<title>React Router Custom Framework</title>
10+
</head>
11+
<body>
12+
<div>
13+
<h1>React Router Custom Framework</h1>
14+
15+
<Form method="post">
16+
<p>
17+
Message: <i>{data.message}</i>
18+
</p>
19+
<fieldset>
20+
<input name="message" placeholder="Enter a new message" />{" "}
21+
<button type="submit">Update</button>
22+
</fieldset>
23+
</Form>
24+
25+
<p>
26+
<Link to="/">Home</Link> | <Link to="/about">About</Link>
27+
</p>
28+
29+
<hr />
30+
31+
<Outlet />
32+
</div>
33+
34+
<script defer src="/js/entry.client.js"></script>
35+
</body>
36+
</html>
37+
);
38+
}

Diff for: app/routes.tsx

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
2+
import About from "./about.js";
3+
import aboutLoader from "./about.loader.js";
4+
import Home from "./home.js";
5+
import Layout from "./layout.js";
6+
7+
let isServer = typeof document === "undefined";
8+
9+
export default [
10+
{
11+
id: "layout",
12+
path: "/",
13+
Component: Layout,
14+
// up to you where your loaders run (client or server), this one dynamically
15+
// imports the correct one to avoid putting the server code in client
16+
// bundles
17+
async loader(args: LoaderFunctionArgs) {
18+
let mod = await (isServer
19+
? import("./layout.server.js")
20+
: import("./layout.client.js"));
21+
return mod.loader(args);
22+
},
23+
// same with the action, you'll probably want to abstract this kind of stuff
24+
// in a createRoute() kind of thing
25+
async action(args: ActionFunctionArgs) {
26+
let mod = await (isServer
27+
? import("./layout.server.js")
28+
: import("./layout.client.js"));
29+
return mod.action(args);
30+
},
31+
children: [
32+
{
33+
id: "home",
34+
index: true,
35+
Component: Home,
36+
},
37+
{
38+
id: "about",
39+
path: "about",
40+
Component: About,
41+
// this loader runs in both places
42+
loader: aboutLoader,
43+
},
44+
],
45+
},
46+
];

Diff for: entry.client.tsx

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { StrictMode } from "react";
2+
import { hydrateRoot } from "react-dom/client";
3+
import { RouterProvider } from "react-router/dom";
4+
import routes from "./app/routes.js";
5+
import { createBrowserRouter } from "react-router";
6+
7+
let router = createBrowserRouter(routes, {
8+
// need to ensure this script runs AFTER <StaticRouterProvider> in
9+
// entry.server.tsx so that window.__staticRouterHydrationData is available
10+
hydrationData: window.__staticRouterHydrationData,
11+
});
12+
13+
hydrateRoot(
14+
document,
15+
<StrictMode>
16+
<RouterProvider router={router} />
17+
</StrictMode>,
18+
);
19+
20+
// type stuff
21+
declare global {
22+
interface Window {
23+
__staticRouterHydrationData: any;
24+
}
25+
}

Diff for: entry.server.tsx

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { renderToString } from "react-dom/server";
2+
import {
3+
createStaticHandler,
4+
createStaticRouter,
5+
StaticRouterProvider,
6+
} from "react-router";
7+
import routes from "./app/routes.js";
8+
import { StrictMode } from "react";
9+
10+
let { query, dataRoutes, queryRoute } = createStaticHandler(routes);
11+
12+
export async function handler(request: Request) {
13+
// Decide if this is a request for data from our client loaders or the initial
14+
// document request for HTML. React Router Vite uses [path].data to make this
15+
// decision, headers could cause problems with a CDN, but it's good for
16+
// illustration here
17+
if (request.headers.get("Accept")?.includes("application/json")) {
18+
return handleDataRequest(request);
19+
} else {
20+
return handleDocumentRequest(request);
21+
}
22+
}
23+
24+
export async function handleDocumentRequest(request: Request) {
25+
// 1. Run action/loaders to get the routing context with `query`
26+
let context = await query(request);
27+
28+
// If `query` returns a Response, send it raw (a route probably a redirected)
29+
if (context instanceof Response) {
30+
return context;
31+
}
32+
33+
// 2. Create a static router for SSR
34+
let router = createStaticRouter(dataRoutes, context);
35+
36+
// 3. Render everything with StaticRouterProvider
37+
let html = renderToString(
38+
<StrictMode>
39+
<StaticRouterProvider router={router} context={context} />
40+
</StrictMode>,
41+
);
42+
43+
// Setup headers from action and loaders from deepest match
44+
let deepestMatch = context.matches[context.matches.length - 1];
45+
let actionHeaders = context.actionHeaders[deepestMatch.route.id];
46+
let loaderHeaders = context.loaderHeaders[deepestMatch.route.id];
47+
48+
let headers = new Headers(actionHeaders);
49+
50+
if (loaderHeaders) {
51+
for (let [key, value] of loaderHeaders.entries()) {
52+
headers.append(key, value);
53+
}
54+
}
55+
56+
headers.set("Content-Type", "text/html; charset=utf-8");
57+
return new Response(`<!DOCTYPE html>${html}`, {
58+
status: context.statusCode,
59+
// 4. send proper headers
60+
headers,
61+
});
62+
}
63+
64+
export async function handleDataRequest(request: Request) {
65+
// 1. we don't want to proxy the browser request directly to our router, so we
66+
// make a new one.
67+
let newRequest =
68+
request.method === "POST"
69+
? new Request(request.url, {
70+
method: request.method,
71+
headers: request.headers,
72+
// @ts-expect-error this is valid, types are wrong
73+
body: new URLSearchParams(await request.formData()),
74+
})
75+
: new Request(request.url, { headers: request.headers });
76+
77+
// 2. get data from our router, queryRoute knows to call the action or loader
78+
// of the leaf route that matches
79+
let data = await queryRoute(newRequest);
80+
81+
// 3. send the response
82+
return new Response(JSON.stringify(data), {
83+
headers: { "Content-Type": "application/json" },
84+
});
85+
}

Diff for: package.json

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "react-router-custom",
3+
"type": "module",
4+
"version": "1.0.0",
5+
"description": "",
6+
"main": "index.js",
7+
"scripts": {
8+
"build:server": "tsc --outDir dist",
9+
"watch:server": "tsc --outDir dist --watch",
10+
"build:client": "esbuild --bundle ./entry.client.tsx --outdir=public/js --sourcemap",
11+
"watch:client": "esbuild --bundle ./entry.client.tsx --outdir=public/js --sourcemap --watch",
12+
"dev": "concurrently 'pnpm run watch:server' 'pnpm run watch:client' 'node --watch ./dist/server.js'",
13+
"start": "pnpm run build:server && pnpm run build:client && node ./dist/server.js"
14+
},
15+
"keywords": [],
16+
"author": "",
17+
"license": "ISC",
18+
"dependencies": {
19+
"@mjackson/node-fetch-server": "^0.3.0",
20+
"@types/react": "^18.3.12",
21+
"esbuild": "^0.24.0",
22+
"react": "^18.3.1",
23+
"react-dom": "^18.3.1",
24+
"react-router": "7.0.0-pre.6"
25+
},
26+
"devDependencies": {
27+
"@types/node": "^22.4.1",
28+
"concurrently": "^9.1.0",
29+
"prettier": "^3.3.3",
30+
"tsimp": "^2.0.11",
31+
"typescript": "^5.5.4"
32+
}
33+
}

0 commit comments

Comments
 (0)