├── public └── static │ └── style.css ├── README.md ├── .gitignore ├── tsconfig.json ├── wrangler.toml ├── package.json ├── vite.config.ts └── src ├── index.tsx └── client.tsx /public/static/style.css: -------------------------------------------------------------------------------- 1 | h1 { font-family: Arial, Helvetica, sans-serif; } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ```txt 2 | npm install 3 | npm run dev 4 | ``` 5 | 6 | ```txt 7 | npm run deploy 8 | ``` 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .wrangler 4 | .dev.vars 5 | 6 | server-build 7 | 8 | # Change them to your taste: 9 | package-lock.json 10 | yarn.lock 11 | pnpm-lock.yaml 12 | bun.lockb -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "lib": [ 8 | "ESNext", 9 | "DOM", 10 | "DOM.Iterable" 11 | ], 12 | "types": [ 13 | "@cloudflare/workers-types", 14 | "vite/client" 15 | ], 16 | "jsx": "react-jsx", 17 | "jsxImportSource": "react" 18 | }, 19 | } -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "hono-spa-react" 2 | compatibility_date = "2024-09-19" 3 | assets = { directory = "./dist/", not_found_handling = "none" } 4 | 5 | [vars] 6 | MY_VAR = "hello" 7 | 8 | # [[kv_namespaces]] 9 | # binding = "MY_KV_NAMESPACE" 10 | # id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 11 | 12 | # [[r2_buckets]] 13 | # binding = "MY_BUCKET" 14 | # bucket_name = "my-bucket" 15 | 16 | # [[d1_databases]] 17 | # binding = "DB" 18 | # database_name = "my-database" 19 | # database_id = "" 20 | 21 | # [ai] 22 | # binding = "AI" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "dev": "vite", 5 | "build": "vite build --mode client && vite build", 6 | "preview": "wrangler dev server-build/index.js", 7 | "deploy": "$npm_execpath run build && wrangler deploy server-build/index.js" 8 | }, 9 | "dependencies": { 10 | "hono": "^4.6.9", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0" 13 | }, 14 | "devDependencies": { 15 | "@cloudflare/workers-types": "^4.20241106.0", 16 | "@hono/vite-build": "^1.1.0", 17 | "@hono/vite-dev-server": "^0.16.0", 18 | "@types/react": "^18.2.60", 19 | "@types/react-dom": "^18.2.19", 20 | "vite": "^5.4.10", 21 | "wrangler": "^3.86.0" 22 | } 23 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import build from '@hono/vite-build/cloudflare-workers' 2 | import devServer from '@hono/vite-dev-server' 3 | import adapter from '@hono/vite-dev-server/cloudflare' 4 | import { defineConfig } from 'vite' 5 | 6 | export default defineConfig(({ mode }) => { 7 | if (mode === 'client') { 8 | return { 9 | build: { 10 | rollupOptions: { 11 | input: './src/client.tsx', 12 | output: { 13 | entryFileNames: 'static/client.js' 14 | } 15 | } 16 | } 17 | } 18 | } else { 19 | return { 20 | ssr: { 21 | external: ['react', 'react-dom'] 22 | }, 23 | plugins: [ 24 | build({ 25 | outputDir: 'server-build' 26 | }), 27 | devServer({ 28 | adapter, 29 | entry: 'src/index.tsx' 30 | }) 31 | ] 32 | } 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | import { renderToString } from 'react-dom/server' 3 | 4 | type Env = { 5 | Bindings: { 6 | MY_VAR: string 7 | } 8 | } 9 | 10 | const app = new Hono() 11 | 12 | app.get('/api/clock', (c) => { 13 | return c.json({ 14 | var: c.env.MY_VAR, // Cloudflare Bindings 15 | time: new Date().toLocaleTimeString() 16 | }) 17 | }) 18 | 19 | app.get('*', (c) => { 20 | return c.html( 21 | renderToString( 22 | 23 | 24 | 25 | 26 | 27 | {import.meta.env.PROD ? ( 28 | 29 | ) : ( 30 | 31 | )} 32 | 33 | 34 |
35 | 36 | 37 | ) 38 | ) 39 | }) 40 | 41 | export default app 42 | -------------------------------------------------------------------------------- /src/client.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import { useState } from 'react' 3 | 4 | function App() { 5 | return ( 6 | <> 7 |

Hello, Hono with React!

8 |

Example of useState()

9 | 10 |

Example of API fetch()

11 | 12 | 13 | ) 14 | } 15 | 16 | function Counter() { 17 | const [count, setCount] = useState(0) 18 | return 19 | } 20 | 21 | const ClockButton = () => { 22 | const [response, setResponse] = useState(null) 23 | 24 | const handleClick = async () => { 25 | const response = await fetch('/api/clock') 26 | const data = await response.json() 27 | const headers = Array.from(response.headers.entries()).reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) 28 | const fullResponse = { 29 | url: response.url, 30 | status: response.status, 31 | headers, 32 | body: data 33 | } 34 | setResponse(JSON.stringify(fullResponse, null, 2)) 35 | } 36 | 37 | return ( 38 |
39 | 40 | {response &&
{response}
} 41 |
42 | ) 43 | } 44 | 45 | const domNode = document.getElementById('root')! 46 | const root = createRoot(domNode) 47 | root.render() 48 | --------------------------------------------------------------------------------