Mục lục
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:
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/
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.
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ụcapp/). 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ệnDOMnhằ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).
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 Packagesyarn remove @vitejs/plugin-react
yarn add react-router
yarn add -D @react-router/dev @react-router/node
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.
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
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 moduleResolution là Bundler (hoặc
NodeNext), sau đó cập nhật types và include:
{
"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 đỏ.
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é!
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 chungTạ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;
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).
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:
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 v4yarn add tailwindcss @tailwindcss/vite
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 },
];
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.
* 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 để testyarn 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'] },
],
},
},
])
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!
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.
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(),
],
});
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.
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ợ.
Thêm lệnh deploy:
"scripts": {
// ...
"deploy": "yarn build && wrangler deploy"
}
- Chạy
yarn dev: Bây giờ Vite dev server của bạn đang chạy thông qua môi trườngworkerdcủ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 loginrồi chạyyarn deploy. Ứng dụng sẽ được build và đẩy lên Edge Network của Cloudflare.