Phase 2: Hệ Sinh Thái React — Routing, State Management, Forms & Data Fetching

Chào mừng đến với Phase 2! Trong Phase 1, bạn đã nắm vững mô hình cốt lõi của React — components, props, state và hooks. Nhưng ứng dụng thực cần nhiều hơn thế. Nó cần nhiều trang, shared global state, forms mạnh mẽ và data fetching hiệu quả.
Phase này giới thiệu hệ sinh thái React — các thư viện đã được kiểm chứng mà React developer dùng trong mọi dự án.
Thời gian cam kết: 5–7 ngày, 1–2 giờ mỗi ngày
Điều kiện tiên quyết: Phase 1: Nền Tảng React
Những Gì Bạn Sẽ Học
Sau khi hoàn thành Phase 2, bạn có thể:
✅ Xây dựng ứng dụng nhiều trang với React Router v6
✅ Tạo protected routes và nested layouts
✅ Chia sẻ state globally với Context API và Zustand
✅ Xử lý forms với validation bằng React Hook Form + Zod
✅ Fetch và cache dữ liệu server với TanStack Query
✅ Xây dựng UI nhanh với Tailwind CSS và shadcn/ui
✅ Xử lý loading, error và empty states nhất quán
Tổng Quan Hệ Sinh Thái React
Mỗi thư viện giải quyết một vấn đề riêng biệt. Bạn sẽ dùng tất cả chúng thường xuyên.
Client-Side Routing với React Router v6
Một single-page application (SPA) cần giả lập nhiều trang mà không tải lại toàn bộ trang. React Router xử lý điều này bằng cách ánh xạ URL paths tới components.
Cài Đặt
npm install react-router-domThiết Lập Cơ Bản
Bọc app trong BrowserRouter và định nghĩa routes:
// main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);// App.jsx
import { Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import Home from './pages/Home';
import Blog from './pages/Blog';
import Post from './pages/Post';
import About from './pages/About';
import NotFound from './pages/NotFound';
export default function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="blog" element={<Blog />} />
<Route path="blog/:slug" element={<Post />} />
<Route path="about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
);
}Điều Hướng và Links
import { Link, NavLink, useNavigate } from 'react-router-dom';
function Navigation() {
const navigate = useNavigate();
return (
<nav>
{/* Link — điều hướng cơ bản */}
<Link to="/">Trang chủ</Link>
{/* NavLink — thêm class 'active' khi route khớp */}
<NavLink
to="/blog"
className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
>
Blog
</NavLink>
{/* Điều hướng theo chương trình */}
<button onClick={() => navigate('/about')}>Giới thiệu</button>
<button onClick={() => navigate(-1)}>Quay lại</button>
</nav>
);
}Nested Routes và Layouts
Tính năng nổi bật của React Router v6: nested routes chia sẻ layouts tự động.
// components/Layout.jsx
import { Outlet, Link } from 'react-router-dom';
export default function Layout() {
return (
<div>
<header>
<Link to="/">MyApp</Link>
<nav>
<Link to="/blog">Blog</Link>
<Link to="/about">Giới thiệu</Link>
</nav>
</header>
<main>
<Outlet /> {/* Child route render ở đây */}
</main>
<footer>© 2026 MyApp</footer>
</div>
);
}// pages/Blog.jsx — với nested routes
import { Outlet } from 'react-router-dom';
export default function Blog() {
return (
<div className="blog-layout">
<aside>
<h2>Danh mục</h2>
{/* sidebar */}
</aside>
<div className="blog-content">
<Outlet /> {/* PostList hoặc Post render ở đây */}
</div>
</div>
);
}Tham Số Route Động
import { useParams } from 'react-router-dom';
function Post() {
const { slug } = useParams(); // Đọc :slug từ URL
return <h1>Bài viết: {slug}</h1>;
}Protected Routes
Bọc các route riêng tư trong component bảo vệ:
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from './hooks/useAuth';
function ProtectedRoute() {
const { user, isLoading } = useAuth();
if (isLoading) return <p>Đang tải...</p>;
if (!user) return <Navigate to="/login" replace />;
return <Outlet />;
}
// Trong App.jsx
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>Query Strings
import { useSearchParams } from 'react-router-dom';
function BlogList() {
const [searchParams, setSearchParams] = useSearchParams();
const page = Number(searchParams.get('page') ?? '1');
const tag = searchParams.get('tag') ?? '';
return (
<div>
<input
value={tag}
onChange={e => setSearchParams({ tag: e.target.value, page: '1' })}
placeholder="Lọc theo tag..."
/>
<p>Trang {page} — Tag: {tag || 'tất cả'}</p>
</div>
);
}Quản Lý Global State
Khi Nào Cần Global State?
Không phải ứng dụng nào cũng cần global state. Hãy bắt đầu mà không có nó và thêm vào khi bạn cảm thấy cần:
- Bạn đang truyền cùng một prop qua 3+ cấp component (prop drilling)
- Nhiều components không liên quan cần cùng dữ liệu (user session, theme, cart)
- Thay đổi state ở một phần của app cần ảnh hưởng đến phần khác
Bắt đầu đơn giản. Chỉ lift up khi bạn thực sự cần.
Context API
Giải pháp tích hợp sẵn của React. Tốt cho các cập nhật không thường xuyên (theme, auth, locale).
import { createContext, useContext, useState } from 'react';
// 1. Tạo context
const ThemeContext = createContext(null);
// 2. Tạo provider component
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggle = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
// 3. Tạo custom hook để dùng sạch sẽ hơn
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme phải được dùng bên trong ThemeProvider');
return context;
}
// 4. Dùng trong bất kỳ component nào
function ThemeToggle() {
const { theme, toggle } = useTheme();
return (
<button onClick={toggle}>
Theme hiện tại: {theme}
</button>
);
}
// 5. Bọc app của bạn
function App() {
return (
<ThemeProvider>
<Router>
{/* ... */}
</Router>
</ThemeProvider>
);
}Hạn chế của Context: Mọi component consume một context sẽ re-render khi bất kỳ giá trị nào trong context thay đổi. Với state cập nhật thường xuyên (như counter hoặc search query), Context gây ra quá nhiều re-renders. Dùng Zustand thay thế.
Zustand
Zustand là điểm cân bằng lý tưởng cho hầu hết ứng dụng — API tối giản, hiệu năng xuất sắc, không boilerplate.
npm install zustandimport { create } from 'zustand';
// Định nghĩa store
const useCartStore = create((set, get) => ({
items: [],
addItem: (product) => set(state => ({
items: [...state.items, { ...product, quantity: 1 }],
})),
removeItem: (id) => set(state => ({
items: state.items.filter(item => item.id !== id),
})),
updateQuantity: (id, quantity) => set(state => ({
items: state.items.map(item =>
item.id === id ? { ...item, quantity } : item
),
})),
clearCart: () => set({ items: [] }),
// Giá trị tính toán qua getters
get totalItems() {
return get().items.reduce((sum, item) => sum + item.quantity, 0);
},
get totalPrice() {
return get().items.reduce((sum, item) => sum + item.price * item.quantity, 0);
},
}));
// Dùng ở bất kỳ đâu — không cần Provider!
function CartIcon() {
const totalItems = useCartStore(state => state.totalItems);
return <span>Giỏ hàng ({totalItems})</span>;
}
function ProductCard({ product }) {
const addItem = useCartStore(state => state.addItem);
return (
<div>
<h3>{product.name}</h3>
<button onClick={() => addItem(product)}>Thêm vào giỏ</button>
</div>
);
}
function Cart() {
const { items, removeItem, clearCart, totalPrice } = useCartStore();
return (
<div>
{items.map(item => (
<div key={item.id}>
<span>{item.name} × {item.quantity}</span>
<button onClick={() => removeItem(item.id)}>Xóa</button>
</div>
))}
<p>Tổng: {totalPrice.toLocaleString('vi-VN')}đ</p>
<button onClick={clearCart}>Xóa giỏ hàng</button>
</div>
);
}Zustand vs Context:
| Context API | Zustand | |
|---|---|---|
| Setup | Nhiều boilerplate hơn | Tối giản |
| Re-renders | Tất cả consumers re-render | Chỉ slices đã subscribe |
| DevTools | Không | Có (Redux DevTools) |
| Phù hợp cho | Theme, auth, locale | Cart, UI state, state phức tạp |
Forms với React Hook Form + Zod
Quản lý form state thủ công với useState rất tẻ nhạt và dễ sai. React Hook Form xử lý điều này hiệu quả với ít re-renders nhất có thể.
Cài Đặt
npm install react-hook-form zod @hookform/resolversForm Cơ Bản
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
// 1. Định nghĩa schema
const schema = z.object({
email: z.string().email('Vui lòng nhập email hợp lệ'),
password: z.string().min(8, 'Mật khẩu phải có ít nhất 8 ký tự'),
confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
message: 'Mật khẩu không khớp',
path: ['confirmPassword'],
});
function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
} = useForm({
resolver: zodResolver(schema),
});
const onSubmit = async (data) => {
try {
await fetch('/api/signup', {
method: 'POST',
body: JSON.stringify(data),
});
reset();
alert('Tạo tài khoản thành công!');
} catch (err) {
console.error(err);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
{...register('email')}
/>
{errors.email && <p className="error">{errors.email.message}</p>}
</div>
<div>
<label htmlFor="password">Mật khẩu</label>
<input
id="password"
type="password"
{...register('password')}
/>
{errors.password && <p className="error">{errors.password.message}</p>}
</div>
<div>
<label htmlFor="confirmPassword">Xác nhận mật khẩu</label>
<input
id="confirmPassword"
type="password"
{...register('confirmPassword')}
/>
{errors.confirmPassword && (
<p className="error">{errors.confirmPassword.message}</p>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Đang tạo tài khoản...' : 'Đăng ký'}
</button>
</form>
);
}Theo Dõi Giá Trị Field
const { register, watch, handleSubmit } = useForm();
const role = watch('role'); // Reactive — re-render khi 'role' thay đổi
return (
<form onSubmit={handleSubmit(onSubmit)}>
<select {...register('role')}>
<option value="user">Người dùng</option>
<option value="admin">Admin</option>
</select>
{role === 'admin' && (
<input
{...register('adminCode')}
placeholder="Mã truy cập admin"
/>
)}
</form>
);Dynamic Fields
import { useFieldArray, useForm } from 'react-hook-form';
function InvoiceForm() {
const { register, control, handleSubmit } = useForm({
defaultValues: { items: [{ name: '', price: 0, qty: 1 }] },
});
const { fields, append, remove } = useFieldArray({
control,
name: 'items',
});
return (
<form onSubmit={handleSubmit(console.log)}>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`items.${index}.name`)} placeholder="Tên sản phẩm" />
<input {...register(`items.${index}.price`)} type="number" placeholder="Giá" />
<input {...register(`items.${index}.qty`)} type="number" placeholder="Số lượng" />
<button type="button" onClick={() => remove(index)}>Xóa</button>
</div>
))}
<button type="button" onClick={() => append({ name: '', price: 0, qty: 1 })}>
Thêm sản phẩm
</button>
<button type="submit">Gửi</button>
</form>
);
}Data Fetching với TanStack Query
useEffect + useState để fetch dữ liệu hoạt động được, nhưng không xử lý caching, refetching, deduplication hay background updates. TanStack Query (trước đây là React Query) làm tất cả và nhiều hơn.
npm install @tanstack/react-queryThiết Lập
// main.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 phút — không refetch nếu dữ liệu còn mới
retry: 2, // Thử lại 2 lần khi request thất bại
},
},
});
createRoot(document.getElementById('root')).render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
);Query Dữ Liệu
import { useQuery } from '@tanstack/react-query';
async function fetchUser(userId) {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Không thể tải thông tin người dùng');
return res.json();
}
function UserProfile({ userId }) {
const { data: user, isLoading, isError, error } = useQuery({
queryKey: ['user', userId], // Cache key duy nhất
queryFn: () => fetchUser(userId),
});
if (isLoading) return <p>Đang tải...</p>;
if (isError) return <p>Lỗi: {error.message}</p>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}Những gì bạn nhận được miễn phí:
- Cache responses — cùng
queryKey= không duplicate requests - Hiển thị dữ liệu cũ ngay lập tức trong khi refetch ở background
- Refetch khi cửa sổ trình duyệt lấy lại focus
- Tự động retry khi thất bại
- Chia sẻ dữ liệu giữa các components — không cần prop drilling
Mutations (POST, PUT, DELETE)
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreatePost() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newPost) =>
fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
}).then(res => res.json()),
onSuccess: () => {
// Invalidate danh sách posts để refetch với bài mới
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
const handleSubmit = (e) => {
e.preventDefault();
const form = e.target;
mutation.mutate({
title: form.title.value,
body: form.body.value,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Tiêu đề" />
<textarea name="body" placeholder="Nội dung" />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Đang tạo...' : 'Tạo bài viết'}
</button>
{mutation.isError && <p>Lỗi: {mutation.error.message}</p>}
{mutation.isSuccess && <p>Tạo bài viết thành công!</p>}
</form>
);
}Paginated Queries
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
function PostList() {
const [page, setPage] = useState(1);
const { data, isLoading, isPlaceholderData } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetch(`/api/posts?page=${page}&limit=10`).then(r => r.json()),
placeholderData: (prev) => prev, // Giữ dữ liệu trang cũ khi tải trang mới
});
return (
<div>
{isLoading ? (
<p>Đang tải...</p>
) : (
<ul>
{data.posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)}
<div className="pagination">
<button
onClick={() => setPage(p => p - 1)}
disabled={page === 1}
>
Trước
</button>
<span>Trang {page}</span>
<button
onClick={() => setPage(p => p + 1)}
disabled={!data?.hasNextPage || isPlaceholderData}
>
Tiếp
</button>
</div>
</div>
);
}TanStack Query vs SWR
Cả hai giải quyết cùng vấn đề. TanStack Query có nhiều tính năng hơn; SWR nhẹ hơn.
| TanStack Query | SWR | |
|---|---|---|
| Bundle size | ~13 KB | ~4 KB |
| Mutations | useMutation first-class | Thủ công |
| Pagination | Tích hợp sẵn | Thủ công |
| DevTools | Có | Không |
| Phù hợp cho | App có nhu cầu data phức tạp | Fetching đơn giản |
Với hầu hết dự án, TanStack Query là lựa chọn mặc định tốt hơn.
UI với Tailwind CSS và shadcn/ui
Tailwind CSS
Tailwind là CSS framework utility-first — thay vì viết file CSS, bạn áp dụng các class có sẵn trực tiếp trong JSX.
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p// Không cần CSS tùy chỉnh
function Button({ children, variant = 'primary' }) {
const base = 'px-4 py-2 rounded-md font-medium transition-colors';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
danger: 'bg-red-600 text-white hover:bg-red-700',
};
return (
<button className={`${base} ${variants[variant]}`}>
{children}
</button>
);
}
function Card({ title, description, imageUrl }) {
return (
<div className="rounded-xl border border-gray-200 overflow-hidden shadow-sm hover:shadow-md transition-shadow">
<img src={imageUrl} alt={title} className="w-full h-48 object-cover" />
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
<p className="mt-1 text-sm text-gray-500">{description}</p>
</div>
</div>
);
}shadcn/ui
shadcn/ui cung cấp các components được thiết kế đẹp và accessible, xây dựng trên Tailwind. Không như thư viện component bạn cài như dependency, shadcn/ui sao chép source code component vào project của bạn — bạn sở hữu code và có thể tùy chỉnh tự do.
npx shadcn@latest init
npx shadcn@latest add button input dialog cardimport { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
function LoginDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Đăng nhập</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Đăng nhập</DialogTitle>
</DialogHeader>
<form className="space-y-4">
<Input type="email" placeholder="Email" />
<Input type="password" placeholder="Mật khẩu" />
<Button type="submit" className="w-full">Đăng nhập</Button>
</form>
</DialogContent>
</Dialog>
);
}Xử Lý Loading, Error và Empty States
Mọi component dựa trên dữ liệu cần ba trạng thái ngoài trường hợp "thành công". Hãy tạo thói quen xử lý tất cả chúng:
function PostList() {
const { data: posts, isLoading, isError, error } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(r => r.json()),
});
// Loading
if (isLoading) {
return (
<div className="space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-24 bg-gray-200 rounded-lg animate-pulse" />
))}
</div>
);
}
// Lỗi
if (isError) {
return (
<div className="rounded-lg bg-red-50 border border-red-200 p-4">
<p className="text-red-800">Không thể tải bài viết: {error.message}</p>
<button className="mt-2 text-red-600 underline">Thử lại</button>
</div>
);
}
// Rỗng
if (posts.length === 0) {
return (
<div className="text-center py-12">
<p className="text-gray-500">Chưa có bài viết nào.</p>
<a href="/new" className="mt-4 text-blue-600">Viết bài đầu tiên →</a>
</div>
);
}
// Thành công
return (
<ul className="space-y-4">
{posts.map(post => (
<li key={post.id}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
);
}Kết Hợp Tất Cả: Ứng Dụng Blog
Đây là cách các công cụ Phase 2 kết hợp trong ứng dụng thực:
Cấu trúc file cho app Phase 2:
src/
├── components/
│ ├── ui/ # shadcn/ui components
│ ├── Layout.jsx
│ └── ProtectedRoute.jsx
├── pages/
│ ├── Home.jsx
│ ├── Blog.jsx
│ ├── Post.jsx
│ └── Dashboard.jsx
├── hooks/
│ ├── useAuth.js
│ └── usePosts.js # Bọc TanStack Query
├── stores/
│ └── cartStore.js # Zustand store
├── lib/
│ └── queryClient.js # TanStack Query client
├── App.jsx
└── main.jsxBài Tập Thực Hành
Bài tập 1 — App nhiều trang Xây dựng app 3 trang (Trang chủ, Sản phẩm, Giới thiệu) với React Router. Thêm nav bar highlight link đang active.
Bài tập 2 — Protected route
Thêm route /profile chuyển hướng đến /login nếu người dùng chưa "đăng nhập" (lưu boolean trong Zustand hoặc Context). Sau khi "đăng nhập", chuyển hướng về /profile.
Bài tập 3 — Tìm kiếm với query params
Xây dựng danh sách có thể tìm kiếm. Lưu search query trong URL (?q=...) với useSearchParams. Tìm kiếm phải tồn tại khi tải lại trang.
Bài tập 4 — Form liên hệ Xây dựng form liên hệ với React Hook Form và Zod. Các trường: tên (bắt buộc), email (email hợp lệ), tin nhắn (tối thiểu 20 ký tự). Hiển thị lỗi từng trường khi người dùng gõ.
Bài tập 5 — Data dashboard
Fetch bài đăng từ https://jsonplaceholder.typicode.com/posts với TanStack Query. Hiển thị skeleton loading state. Thêm input tìm kiếm lọc kết quả hiển thị. Hiển thị empty state khi không có kết quả nào khớp.
Tóm Tắt
Phase 2 cung cấp cho bạn bộ công cụ cho ứng dụng React thực:
| Công Cụ | Vấn Đề Giải Quyết |
|---|---|
| React Router v6 | Điều hướng nhiều trang, nested layouts, protected routes |
| Context API | Global state cập nhật thấp (theme, auth, locale) |
| Zustand | Shared state không prop drilling và không vấn đề re-render |
| React Hook Form + Zod | Forms với validation, không quản lý state thủ công |
| TanStack Query | Server state — caching, background refetching, mutations |
| Tailwind CSS | Styling không rời khỏi JSX |
| shadcn/ui | Thư viện component accessible và có thể tùy chỉnh |
Đây là các công cụ bạn sẽ dùng trong hầu như mọi dự án React. Học một lần, áp dụng ở khắp nơi.
Tiếp Theo Là Gì
Trong Bài RN-4: Phase 3 — Next.js Full-Stack, bạn sẽ đưa kiến thức React này vào Next.js:
- File-based routing với App Router
- Server-Side Rendering và Static Generation
- React Server Components
- API Route Handlers và Server Actions
- Tối ưu hóa image, font và script tích hợp sẵn
Chỉ Mục Series
| Bài | Tiêu Đề | Trạng Thái |
|---|---|---|
| RN-1 | React.js & Next.js Roadmap | ✅ Hoàn thành |
| RN-2 | Phase 1: Nền Tảng React | ✅ Hoàn thành |
| RN-3 | Phase 2: Hệ Sinh Thái React | ✅ Bạn đang ở đây |
| RN-4 | Phase 3: Next.js Full-Stack | Sắp Ra Mắt |
| RN-5 | Deep Dive: Thành Thạo React Hooks | Sắp Ra Mắt |
| RN-6 | Deep Dive: State Management | Sắp Ra Mắt |
| RN-7 | Deep Dive: App Router & Server Components | Sắp Ra Mắt |
| RN-8 | Deep Dive: Các Pattern Data Fetching | Sắp Ra Mắt |
| RN-9 | Deep Dive: Tối Ưu Hiệu Năng | Sắp Ra Mắt |
| RN-10 | Deep Dive: Authentication trong Next.js | Sắp Ra Mắt |
| RN-11 | Deep Dive: Testing React & Next.js | Sắp Ra Mắt |
| RN-12 | Deep Dive: Deploy Ứng Dụng Next.js | Sắp Ra Mắt |
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.