Skip to content

Commit db47cf9

Browse files
kristianfreemanShaun Persad
andauthored
Add react-router-hono-fullstack-template (#473)
Co-authored-by: Shaun Persad <spersad@cloudflare.com>
1 parent 0726431 commit db47cf9

26 files changed

+13035
-24
lines changed

pnpm-lock.yaml

Lines changed: 540 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.DS_Store
2+
/node_modules/
3+
*.tsbuildinfo
4+
5+
# React Router
6+
/.react-router/
7+
/build/
8+
9+
# Cloudflare
10+
.mf
11+
.wrangler
12+
.dev.vars*
13+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"files.associations": {
3+
"wrangler.json": "jsonc"
4+
}
5+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Hono + React Router + Vite + ShadCN UI on Cloudflare Workers
2+
3+
[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/templates/tree/staging/react-router-hono-fullstack-template)
4+
5+
![Build modern full-stack apps with Hono, React Router, and ShadCN UI on Cloudflare Workers](https://imagedelivery.net/wSMYJvS3Xw-n339CbDyDIA/24c5a7dd-e1e3-43a9-b912-d78d9a4293bc/public)
6+
7+
<!-- dash-content-start -->
8+
9+
A modern full-stack template powered by [Cloudflare Workers](https://workers.cloudflare.com/), using [Hono](https://hono.dev/) for backend APIs, [React Router](https://reactrouter.com/) for frontend routing, and [shadcn/ui](https://ui.shadcn.com/) for beautiful, accessible components styled with [Tailwind CSS](https://tailwindcss.com/).
10+
11+
Built with the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/) for optimized static asset delivery and seamless local development. React is configured in single-page app (SPA) mode via Workers.
12+
13+
A perfect starting point for building interactive, styled, and edge-deployed SPAs with minimal configuration.
14+
15+
## Features
16+
17+
- ⚡ Full-stack app on Cloudflare Workers
18+
- 🔁 Hono for backend API endpoints
19+
- 🧭 React Router for client-side routing
20+
- 🎨 ShadCN UI with Tailwind CSS for components and styling
21+
- 🧱 File-based route separation
22+
- 🚀 Zero-config Vite build for Workers
23+
- 🛠️ Automatically deploys with Wrangler
24+
25+
<!-- dash-content-end -->
26+
27+
## Tech Stack
28+
29+
- **Frontend**: React + React Router + ShadCN UI
30+
31+
- SPA architecture powered by React Router
32+
- Includes accessible, themeable UI from ShadCN
33+
- Styled with utility-first Tailwind CSS
34+
- Built and optimized with Vite
35+
36+
- **Backend**: Hono on Cloudflare Workers
37+
38+
- API routes defined and handled via Hono in `/api/*`
39+
- Supports REST-like endpoints, CORS, and middleware
40+
41+
- **Deployment**: Cloudflare Workers via Wrangler
42+
- Vite plugin auto-bundles frontend and backend together
43+
- Deployed worldwide on Cloudflare’s edge network
44+
45+
## Resources
46+
47+
- 🧩 [Hono on Cloudflare Workers](https://hono.dev/docs/getting-started/cloudflare-workers)
48+
- 📦 [Vite Plugin for Cloudflare](https://developers.cloudflare.com/workers/vite-plugin/)
49+
- 🛠 [Wrangler CLI reference](https://developers.cloudflare.com/workers/wrangler/)
50+
- 🎨 [shadcn/ui](https://ui.shadcn.com)
51+
- 💨 [Tailwind CSS Documentation](https://tailwindcss.com/)
52+
- 🔀 [React Router Docs](https://reactrouter.com/)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@import "tailwindcss" source(".");
2+
3+
@theme {
4+
--font-sans:
5+
"Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
6+
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
7+
}
8+
9+
html,
10+
body {
11+
@apply bg-white dark:bg-gray-950;
12+
13+
@media (prefers-color-scheme: dark) {
14+
color-scheme: dark;
15+
}
16+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { AppLoadContext, EntryContext } from "react-router";
2+
import { ServerRouter } from "react-router";
3+
import { isbot } from "isbot";
4+
import { renderToReadableStream } from "react-dom/server";
5+
6+
export default async function handleRequest(
7+
request: Request,
8+
responseStatusCode: number,
9+
responseHeaders: Headers,
10+
routerContext: EntryContext,
11+
_loadContext: AppLoadContext,
12+
) {
13+
let shellRendered = false;
14+
const userAgent = request.headers.get("user-agent");
15+
16+
const body = await renderToReadableStream(
17+
<ServerRouter context={routerContext} url={request.url} />,
18+
{
19+
onError(error: unknown) {
20+
responseStatusCode = 500;
21+
// Log streaming rendering errors from inside the shell. Don't log
22+
// errors encountered during initial shell rendering since they'll
23+
// reject and get logged in handleDocumentRequest.
24+
if (shellRendered) {
25+
console.error(error);
26+
}
27+
},
28+
},
29+
);
30+
shellRendered = true;
31+
32+
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
33+
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
34+
if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {
35+
await body.allReady;
36+
}
37+
38+
responseHeaders.set("Content-Type", "text/html");
39+
return new Response(body, {
40+
headers: responseHeaders,
41+
status: responseStatusCode,
42+
});
43+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {
2+
isRouteErrorResponse,
3+
Links,
4+
Meta,
5+
Outlet,
6+
Scripts,
7+
ScrollRestoration,
8+
} from "react-router";
9+
10+
import type { Route } from "./+types/root";
11+
import "./app.css";
12+
13+
export const links: Route.LinksFunction = () => [
14+
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
15+
{
16+
rel: "preconnect",
17+
href: "https://fonts.gstatic.com",
18+
crossOrigin: "anonymous",
19+
},
20+
{
21+
rel: "stylesheet",
22+
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
23+
},
24+
];
25+
26+
export function Layout({ children }: { children: React.ReactNode }) {
27+
return (
28+
<html lang="en">
29+
<head>
30+
<meta charSet="utf-8" />
31+
<meta name="viewport" content="width=device-width, initial-scale=1" />
32+
<Meta />
33+
<Links />
34+
</head>
35+
<body>
36+
{children}
37+
<ScrollRestoration />
38+
<Scripts />
39+
</body>
40+
</html>
41+
);
42+
}
43+
44+
export default function App() {
45+
return <Outlet />;
46+
}
47+
48+
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
49+
let message = "Oops!";
50+
let details = "An unexpected error occurred.";
51+
let stack: string | undefined;
52+
53+
if (isRouteErrorResponse(error)) {
54+
message = error.status === 404 ? "404" : "Error";
55+
details =
56+
error.status === 404
57+
? "The requested page could not be found."
58+
: error.statusText || details;
59+
} else if (import.meta.env.DEV && error && error instanceof Error) {
60+
details = error.message;
61+
stack = error.stack;
62+
}
63+
64+
return (
65+
<main className="pt-16 p-4 container mx-auto">
66+
<h1>{message}</h1>
67+
<p>{details}</p>
68+
{stack && (
69+
<pre className="w-full p-4 overflow-x-auto">
70+
<code>{stack}</code>
71+
</pre>
72+
)}
73+
</main>
74+
);
75+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { type RouteConfig, index } from "@react-router/dev/routes";
2+
3+
export default [index("routes/home.tsx")] satisfies RouteConfig;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Route } from "./+types/home";
2+
import { Welcome } from "../welcome/welcome";
3+
4+
export function meta({}: Route.MetaArgs) {
5+
return [
6+
{ title: "New React Router App" },
7+
{ name: "description", content: "Welcome to React Router!" },
8+
];
9+
}
10+
11+
export function loader({ context }: Route.LoaderArgs) {
12+
return { message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE };
13+
}
14+
15+
export default function Home({ loaderData }: Route.ComponentProps) {
16+
return <Welcome message={loaderData.message} />;
17+
}
Lines changed: 23 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)