diff --git a/www/src/content/blog/2024-12-06-client-side-guide.mdx b/www/src/content/blog/2024-12-06-client-side-guide.mdx new file mode 100644 index 000000000..fd8aa0987 --- /dev/null +++ b/www/src/content/blog/2024-12-06-client-side-guide.mdx @@ -0,0 +1,621 @@ +--- +title: "Step-by-step guide: Adding client-side logic to your Hono app" +description: A guide to adding client-side logic to your Hono app. +slug: client-side-guide +date: 2024-12-06 +author: Oscar van Zijverden +tags: + - guide + - Hono + - hono.js + - typescript + - hydration +--- + +import { + Aside, + Card, + FileTree, + TabItem, + Tabs +} from "@astrojs/starlight/components"; +import videoHydrated from "../../assets/blog/2024-12-06-client-side-guide-hydrated.mov"; +import videoStatic from "../../assets/blog/2024-12-06-client-side-guide-static.mov"; + +[Hono](https://hono.dev) is a great framework to build serverless apps with +familiar APIs using Web Standards. It comes with a ton of features out of the +box. + +One of these features is the ability to compose & render HTML server-side, which +is great for static content.\ +But what if you want to add some client-side logic to your app? +You can do so by using the hooks provided by Hono. However, they might not work +in your browser — that's because we're not shipping any JavaScript to the +browser! + + + +In this guide we'll go over how to build an Hono app with client-side logic, +unlocking the full potential of your projects. + +## What are we building? + +We're building a simple app that renders a counter component server-side and +hydrates it client-side.\ +It runs in Cloudflare Workers, leveraging its +[static asset bindings](https://developers.cloudflare.com/workers/static-assets) +\- though the same principles apply to the other supported environments as well. + +Using [Vite](https://vitejs.dev) we'll set up two build steps: one for the +client-side logic and one for the server-side logic. + +
+ + +
+ The final result: a counter component that increments a count when a button + is clicked! +
+
+ +## Let's build! + +First, let's get started with scaffolding a new Hono app. + + + + +```sh +npm create hono@latest hono-client +``` + + + + + +```sh +yarn create hono hono-client +``` + + + + + +```sh +pnpm create hono hono-client +``` + + + + + +```sh +bunx create-hono hono-client +``` + + + + + + +The `src` directory contains a single `index.ts` file with a simple Hono app. +We're adding a `client` directory with an index and component: + + + +- src + - index.ts + - **client** + - **index.tsx** logic to mount the app on the client + - **Counter.tsx** component to demonstrate client-side logic + + + +### Adding the component & mounting point + +Let's start by setting up a simple counter component that increments a count +when a button is clicked: + +```tsx title="src/client/Counter.tsx" +import { useState } from "hono/jsx"; + +export function Counter() { + const [count, setCount] = useState(0); + + return ( +
+ + Count: {count} +
+ ); +} +``` + +Then we import the component & hydrate it in the client entry file: + +```tsx title="src/client/index.tsx" +import { StrictMode } from "hono/jsx"; +import { hydrateRoot } from "hono/jsx/dom/client"; + +import { Counter } from "./Counter"; + +const root = document.getElementById("root"); +if (!root) { + throw new Error("Root element not found"); +} + +hydrateRoot( + root, + + + +); +``` + + + +Your code editor might give you a hint that `document` is not defined. Given we +added the client-side logic, we need to tell TypeScript that we're running in a +browser environment: + +```json title="tsconfig.json" ins={6} +{ + "compilerOptions": { + //... + "lib": [ + "ESNext", + "DOM" + ], + // ... + } +} +``` + +We're going to add some JSX to the `src/index.ts` file, so we first need to +change the file extension to `.tsx`. +Once that's done, we can add Hono's +[JSX renderer middleware](https://hono.dev/docs/middleware/builtin/jsx-renderer) +to the `/` route and return the statically rendered `` component: + +```tsx title="src/index.tsx" ins={2, 4, 8-23, 26} del={25} +import { Hono } from "hono"; +import { jsxRenderer } from "hono/jsx-renderer"; + +import { Counter } from "./client/Counter"; + +const app = new Hono(); + +app.use( + jsxRenderer( + ({ children }) => ( + + + + + hono-client + +
{children}
+ + ), + { docType: true } + ) +); + +app.get("/", (c) => { + return c.text("Hello Hono!"); + return c.render(); +}); + +export default app; +``` + +We're almost there. If you run the app now with the `dev` script, you'll get an +error. Let's fix that by adding the build steps! + +### Adding build steps and scripts + +At this point, we have both server-side and client-side logic and need to add +two build steps to our project. Let's install Vite and two plugins to facilitate +this. + + + + +```sh +npm install vite +npm install -D @hono/vite-build @hono/vite-dev-server +``` + + + + + + ```sh + yarn add vite + yarn add -D @hono/vite-build @hono/vite-dev-server + ``` + + + + + + ```sh + pnpm add vite + pnpm add -D @hono/vite-build @hono/vite-dev-server + ``` + + + + + +```sh +bun add vite +bun add -D @hono/vite-build @hono/vite-dev-server +``` + + + + +In the root of your project, create a `vite.config.ts` file. We'll define the +config for both the client-side build and the server-side build: + +```ts title="vite.config.ts" +import build from "@hono/vite-build/cloudflare-workers"; +import devServer from "@hono/vite-dev-server"; +import cloudflareAdapter from "@hono/vite-dev-server/cloudflare"; +import { defineConfig } from "vite"; + +export default defineConfig(({ mode }) => { + if (mode === "client") { + return { + build: { + rollupOptions: { + input: "./src/client/index.tsx", + output: { + entryFileNames: "assets/[name].js", + }, + }, + outDir: "./public" + } + }; + } + + const entry = "./src/index.tsx"; + return { + server: { port: 8787 }, + plugins: [ + devServer({ adapter: cloudflareAdapter, entry }), + build({ entry }) + ] + }; +}); +``` + + + +Now we need to adjust the `package.json` scripts to facilitate the new build +steps. Additionally, we set the type to `module` to allow for ESM imports: + +```json title="package.json" del={5} ins={3, 6, 7} +{ + "name": "hono-client", + "type": "module", + "scripts": { + "dev": "wrangler dev", + "dev": "vite dev", + "build": "vite build --mode client && vite build", + "deploy": "wrangler deploy --minify" + } + // ... +} +``` + + + +#### Running the app +If you run the app now with the `dev` script, you'll see the counter component +rendered server-side. The client-side script hasn't been loaded yet, so the +counter component won't work. + + + + +```sh +npm run dev +``` + + + + + +```sh +yarn dev +``` + + + + + +```sh +pnpm dev +``` + + + + + +```sh +bun dev +``` + + + + + + +There's only one step left to make the counter component work. We're almost +there! + +### Load the client-side script + +As a final step we need to load the client-side script in the document's head. + +For the script that we're loading we need to make a distinction between a +development and production environment. Vite allows us to do this easily with +its [built-in env](https://vite.dev/guide/env-and-mode#env-variables). +For the dev environment we can load the client's `.tsx` file; for production we +have to read it from the `public` directory. + +First we add the `vite/client` types to the TypeScript config: + +```json title="tsconfig.json" ins={6} +{ + "compilerOptions": { + //... + "types": [ + // ... + "vite/client" + ] + //... + } +} +``` + +Then we adjust the `src/index.tsx` file to load the client-side script, +depending on the environment: + +```tsx title="src/index.tsx" ins={11-18} +// ... +app.use( + jsxRenderer( + ({ children }) => ( + + + + + hono-client + +