Thiết lập Fullstack React Router v7 trên Cloudflare Workers

Hướng dẫn chuẩn 5 bước: Khởi tạo -> React Router v7 -> Routing -> Tailwind v4 & Shadcn -> Wrangler Deploy. Mỗi bước đều có thể chạy thử để xác nhận thành công. Kèm theo giải thích chi tiết mục đích của từng cấu hình.

13/05/2026
10 phút đọc
1

Khởi tạo dự án

Mặc dù React Router có công cụ create-react-router, việc bắt đầu từ một dự án Vite cơ bản và nâng cấp thủ công giúp chúng ta kiểm soát hoàn toàn cấu trúc và hiểu rõ cách các thành phần liên kết với nhau.

yarn create vite web --template react-ts
cd web
yarn

1. Cấu trúc Mặc định của Vite

Ngay sau khi chạy lệnh khởi tạo, cấu trúc đầy đủ của dự án (SPA mặc định) sẽ trông chính xác như sau:

web/ ├── public/ │ ├── favicon.svg │ └── icons.svg ├── src/ │ ├── assets/ │ │ ├── hero.png │ │ ├── react.svg │ │ └── vite.svg │ ├── App.css │ ├── App.tsx │ ├── index.css │ └── main.tsx # Điểm vào (entry point) của React ├── .gitignore ├── eslint.config.js ├── index.html # Tệp HTML gốc (entry point của Vite) ├── package.json ├── README.md ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts

2. Dọn dẹp Dự án Cũ

Xoá các thư mục và tệp không cần thiết của kiến trúc Single Page App (SPA) cũ để chuẩn bị không gian cho Framework Mode:

rm index.html
rm -rf src/
Tại sao phải xoá index.html và src/?

Trong kiến trúc SPA mặc định, index.html là điểm bắt đầu duy nhất và trình duyệt sẽ tải toàn bộ JavaScript từ đó. Tuy nhiên, React Router v7 Framework Mode sử dụng Server-Side Rendering (SSR). Tệp HTML sẽ được tạo động trên máy chủ (thông qua root.tsx). Thư mục src/ cũng sẽ được loại bỏ và thay thế bằng thư mục app/ theo đúng quy chuẩn cấu trúc mới.

DONE ✔️

Dự án đã được dọn sạch (hiện tại thư mục src/ và file index.html đã hoàn toàn biến mất), sẵn sàng như một tờ giấy trắng để xây dựng kiến trúc mới.

Kiến trúc Cấu hình TypeScript (Project References)

Dự án Vite hiện đại được khởi tạo mặc định với 3 tệp cấu hình TypeScript nhằm phân tách môi trường thực thi, đảm bảo tính chặt chẽ trong quá trình kiểm tra kiểu (type-checking):

  • tsconfig.json (Tệp điều hướng gốc): Không chứa trực tiếp cấu hình biên dịch mà sử dụng kỹ thuật Project References ("references": [...]) để liên kết các tệp cấu hình con. Các công cụ (như VSCode, tsc) sẽ đọc tệp này đầu tiên để nhận diện các vùng môi trường độc lập.
  • tsconfig.app.json (Môi trường Trình duyệt): Dành riêng cho mã nguồn giao diện (nằm trong thư mục app/). Tệp này được cấu hình với "lib": ["DOM"], cấp phép sử dụng các Web APIs toàn cục (như window, document).
  • tsconfig.node.json (Môi trường Node.js): Áp dụng cho các tệp cấu hình ở cấp độ gốc (như vite.config.ts, react-router.config.ts). Môi trường này loại bỏ thư viện DOM nhằm ngăn chặn các rủi ro phát sinh do việc gọi nhầm API trình duyệt trong mã thực thi phía máy chủ (build scripts).
2

Cài đặt React Router v7 Framework Mode

Trong bước này, chúng sẽ biến ứng dụng React thuần thành một ứng dụng Fullstack (có Server-Side Rendering).

1. Cài đặt Packages
yarn remove @vitejs/plugin-react
yarn add react-router
yarn add -D @react-router/dev @react-router/node
Tại sao gỡ @vitejs/plugin-react?

Plugin @react-router/dev/vite đã bao gồm sẵn khả năng biên dịch React, đồng thời bổ sung thêm các tính năng như file-based routing, SSR, và code-splitting. Giữ lại plugin cũ sẽ gây xung đột.

2. Cấu hình Vite & React Router

Cập nhật vite.config.ts:

import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";

export default defineConfig({
  plugins: [reactRouter()],
});

Tạo file react-router.config.ts ở thư mục gốc web/:

import type { Config } from "@react-router/dev/config";

export default {
  ssr: true,
} satisfies Config;
3. Tạo cấu trúc thư mục app/

Tạo thư mục app/ và các file nền tảng:

app/root.tsx (Layout chính)

Thay thế cho file index.html. Framework sẽ tự động chèn các tags meta, styles, và scripts thông qua các component <Meta />, <Links />, <Scripts />.

import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";

export default function Root() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

app/entry.client.tsx

Được thực thi trên trình duyệt. Quá trình hydrateRoot sẽ "đánh thức" mã HTML tĩnh mà server trả về thành một ứng dụng React tương tác.

import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";

hydrateRoot(document, <HydratedRouter />);

app/entry.server.tsx

Tại sao dùng Web Streams?

Mặc định React dùng API renderToPipeableStream (chuẩn Node.js). Nhưng vì chúng ta sẽ deploy lên Cloudflare Workers (dùng V8 engine), ta bắt buộc phải sử dụng renderToReadableStream (chuẩn Web Streams) để render HTML trên server.

import type { EntryContext } from "react-router";
import { ServerRouter } from "react-router";
import { renderToReadableStream } from "react-dom/server";

export default async function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  entryContext: EntryContext
) {
  const stream = await renderToReadableStream(
    <ServerRouter context={entryContext} url={request.url} />,
    {
      signal: request.signal,
      onError(error: unknown) {
        console.error(error);
        responseStatusCode = 500;
      },
    }
  );
  responseHeaders.set("Content-Type", "text/html");
  return new Response(stream, { headers: responseHeaders, status: responseStatusCode });
}
4. Khởi tạo Route đầu tiên (Trang chủ)
// app/routes.ts
import { type RouteConfig, route } from "@react-router/dev/routes";
export default [ route("/", "routes/home.tsx") ] satisfies RouteConfig;
// app/routes/home.tsx
export default function Home() {
  return <h1>Hello React Router v7</h1>;
}
5. Cập nhật Typescript

Mở tsconfig.app.json, đảm bảo moduleResolutionBundler (hoặc NodeNext), sau đó cập nhật typesinclude:

{
  "compilerOptions": {
    // ...
    "types": ["@react-router/node", "vite/client"],
    "moduleResolution": "bundler"
  },
  "include": ["app"]
}

Đồng thời, thêm file "react-router.config.ts" vào mảng include của tsconfig.node.json để tránh lỗi báo đỏ.

DONE ✔️ Chạy thử

Chạy yarn dev và truy cập http://localhost:5173. Nếu bạn thấy màn hình "Hello React Router v7" được render ngay từ mã nguồn trang (View Page Source), chúc mừng bạn đã thiết lập thành công SSR!


Lưu ý quan trọng: Ngay khi chạy lệnh yarn dev, React Router sẽ tự động sinh ra thư mục ẩn .react-router/. Đừng quên mở file .gitignore và thêm dòng .react-router vào cuối file để tránh commit nhầm các file build này lên Git nhé!

3

Thiết lập Routing Cơ bản (3 routes)

Để hiểu rõ cách khai báo route trong React Router v7, chúng ta sẽ tạo 3 trang cơ bản: Home, About, và Contact, cùng với một Layout chứa thanh điều hướng.

1. Tạo Layout chung

Tạo file app/routes/layout.tsx. File này sử dụng <Outlet /> để render nội dung của các route con và <NavLink /> để chuyển trang không cần reload (SPA navigation).

import { NavLink, Outlet } from "react-router";

export default function MainLayout() {
  return (
    <div>
      <nav style={{ display: 'flex', gap: '1rem', padding: '1rem', background: '#eee' }}>
        <NavLink to="/">Trang chủ</NavLink>
        <NavLink to="/about">Giới thiệu</NavLink>
        <NavLink to="/contact">Liên hệ</NavLink>
      </nav>
      <div style={{ padding: '1rem' }}>
        <Outlet />
      </div>
    </div>
  );
}
2. Tạo các trang nội dung

Tạo các file tương ứng trong thư mục app/routes/:

// app/routes/about.tsx
export default function About() {
  return <h1>Trang Giới Thiệu</h1>;
}

// app/routes/contact.tsx
export default function Contact() {
  return <h1>Trang Liên Hệ</h1>;
}
3. Đăng ký Routes

Mở file app/routes.ts. Sử dụng layout() để bọc các route() con bên trong:

import { type RouteConfig, index, layout, route } from "@react-router/dev/routes";

export default [
  layout("routes/layout.tsx", [
    index("routes/home.tsx"),
    route("about", "routes/about.tsx"),
    route("contact", "routes/contact.tsx")
  ])
] satisfies RouteConfig;
Lưu ý:

Hàm index() sẽ thiết lập route mặc định hiển thị khi đường dẫn khớp chính xác với thư mục cha (ở đây là /). Các đường dẫn khác khai báo bằng route() sẽ được sinh ra tương ứng (/about, /contact).

DONE ✔️ Chạy thử

Mở trình duyệt, thử click vào các menu điều hướng. Bạn sẽ thấy việc chuyển trang diễn ra mượt mà và tức thì nhờ SPA Navigation, nhưng khi bạn F5 trang, server vẫn trả về HTML được render chính xác cho nội dung của trang đó nhờ cơ chế SSR.

Kiến trúc Thư mục Hiện tại

Đến đây, bạn đã hoàn thiện bộ khung cơ bản của React Router v7. Hãy đối chiếu cây thư mục của bạn với cấu trúc chuẩn dưới đây:

web/ ├── app/ │ ├── entry.client.tsx # Khởi tạo React trên trình duyệt │ ├── entry.server.tsx # Render HTML trên máy chủ │ ├── root.tsx # Layout gốc toàn trang │ ├── routes.ts # Đăng ký định tuyến tập trung │ └── routes/ # Giao diện các trang con │ ├── layout.tsx │ ├── home.tsx │ ├── about.tsx │ └── contact.tsx ├── public/ ├── package.json ├── react-router.config.ts # Cấu hình SSR cho React Router ├── tsconfig.app.json ├── tsconfig.node.json ├── tsconfig.json └── vite.config.ts
4

Cài đặt Tailwind CSS v4 & Shadcn

Tích hợp Tailwind CSS phiên bản mới nhất và thư viện component Shadcn.

1. Cài đặt Tailwind CSS v4
yarn add tailwindcss @tailwindcss/vite
Tại sao Tailwind v4 lại cài dưới dạng Vite plugin?

Khác với v3 sử dụng PostCSS, Tailwind v4 đã thay đổi kiến trúc để trở thành một Vite plugin gốc. Điều này mang lại tốc độ biên dịch cực nhanh và loại bỏ nhu cầu sử dụng file tailwind.config.js phức tạp. Mọi cấu hình giờ đây nằm trực tiếp trong file CSS.

Cập nhật vite.config.ts:

import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [tailwindcss(), reactRouter()],
});

Tạo file app/app.css và thêm thư viện Tailwind:

@import "tailwindcss";

Import CSS vào app/root.tsx thông qua hàm links():

import type { LinksFunction } from "react-router";
import appStylesHref from "./app.css?url";

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: appStylesHref },
];
Tại sao import CSS qua hàm links?

Việc export hàm links báo cho React Router biết cần tải file CSS này ở thẻ <head> của HTML trước khi nội dung được render. Điều này đảm bảo không có hiện tượng chớp nhoáng giao diện (Flash of Unstyled Content - FOUC) khi dùng SSR.

2. Khởi tạo Shadcn

* Lưu ý: Chỉ thực hiện phần này (và bước 3) nếu dự án của bạn có nhu cầu sử dụng bộ component UI của Shadcn. Nếu không, bạn hoàn toàn có thể bỏ qua để giữ mã nguồn tối giản nhất.

yarn dlx shadcn@latest init

Lệnh này sẽ tự động tải các util (như `cn`), cấu hình alias paths, và sẵn sàng thư mục để bạn thả component vào.

3. Thêm một Component để test
yarn dlx shadcn@latest add button

Cập nhật app/routes/home.tsx để test Button của Shadcn:

import { Button } from "../components/ui/button";

export default function Home() {
  return (
    <div class="p-8 space-y-4">
      <h1 class="text-2xl font-bold text-blue-500">Tailwind v4 is working!</h1>
      <Button>Shadcn Button</Button>
    </div>
  );
}
4. Cấu hình ESLint (Khắc phục cảnh báo Fast Refresh)

Khi sử dụng Tailwind hoặc thao tác cấu hình route, việc export các hàm như links, meta, loader từ component sẽ khiến plugin react-refresh cảnh báo vàng. Mở file eslint.config.js và thêm whitelist sau vào phần rules:

export default defineConfig([
  // ...
  {
    files: ['**/*.{ts,tsx}'],
    // ...
    rules: {
      'react-refresh/only-export-components': [
        'warn',
        { allowExportNames: ['meta', 'links', 'headers', 'loader', 'action'] },
      ],
    },
  },
])
DONE ✔️ Hoàn thiện Giao diện

Khởi động lại yarn dev. Nếu trang hiển thị chữ màu xanh và có một Button UI chuẩn của Shadcn, đồng thời terminal không còn báo vàng ESLint, bạn đã hoàn tất phần giao diện!

5

Thiết lập Môi trường Cloudflare Workers

Bước cuối cùng là đảm bảo dự án có thể chạy và deploy trên Cloudflare Workers. Cloudflare sử dụng workerd - một runtime V8 nhẹ hơn rất nhiều so với Node.js.

1. Cài đặt Plugins
yarn add -D wrangler @cloudflare/vite-plugin
2. Kích hoạt Cloudflare Plugins

Mở vite.config.ts và thêm plugin Cloudflare (phải đặt trước reactRouter):

import { cloudflare } from "@cloudflare/vite-plugin";

export default defineConfig({
  plugins: [
    cloudflare({ viteEnvironment: { name: "ssr" } }),
    tailwindcss(),
    reactRouter(),
  ],
});
Tác dụng của Cloudflare Vite Plugin?

Bình thường yarn dev sẽ chạy code server-side trên Node.js. Nhưng sản phẩm thật trên Cloudflare sẽ chạy trên workerd. Plugin này sẽ tạo ra một môi trường giả lập workerd ngay trong lúc phát triển, giúp bạn phát hiện sớm các lỗi không tương thích API (như việc sử dụng module fs của Node) và hỗ trợ truy cập các binding (KV, D1, R2) của Cloudflare một cách chuẩn xác.

Mở react-router.config.ts và kích hoạt Vite Environment API:

export default {
  ssr: true,
  future: {
    v8_viteEnvironmentApi: true,
  },
} satisfies Config;

Cờ v8_viteEnvironmentApi cho phép React Router tích hợp sâu với hệ thống đa môi trường (Client/Server) mới của Vite 6, điều kiện tiên quyết để Cloudflare plugin hoạt động.

3. Cấu hình Wrangler

Tạo file wrangler.jsonc ở thư mục gốc web/. Đây là file cấu hình mà Cloudflare Servers sẽ đọc khi bạn deploy.

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "survi-vn-web",
  "compatibility_date": "2026-05-13",
  "main": "build/server/index.js",
  "assets": {
    "directory": "build/client"
  },
  "compatibility_flags": ["nodejs_compat"]
}

Trường main chỉ tới file server bundle do React Router build ra. Cờ nodejs_compat cho phép worker sử dụng một phần nhỏ các Node.js core modules đã được Cloudflare hỗ trợ.

4. Cập nhật Package.json

Thêm lệnh deploy:

"scripts": {
  // ...
  "deploy": "yarn build && wrangler deploy"
}
DONE ✔️ Chạy thử & Deploy
  • Chạy yarn dev: Bây giờ Vite dev server của bạn đang chạy thông qua môi trường workerd của Cloudflare! Log server sẽ hiển thị khác so với Vite thông thường.
  • Khi bạn đã sẵn sàng chia sẻ ứng dụng, chạy yarn wrangler login rồi chạy yarn deploy. Ứng dụng sẽ được build và đẩy lên Edge Network của Cloudflare.