Back to blog

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

reactreact-routerzustandtanstack-queryfrontend
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-dom

Thiế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>
  );
}
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 zustand
import { 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 APIZustand
SetupNhiều boilerplate hơnTối giản
Re-rendersTất cả consumers re-renderChỉ slices đã subscribe
DevToolsKhôngCó (Redux DevTools)
Phù hợp choTheme, auth, localeCart, 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/resolvers

Form 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-query

Thiế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 QuerySWR
Bundle size~13 KB~4 KB
MutationsuseMutation first-classThủ công
PaginationTích hợp sẵnThủ công
DevToolsCóKhông
Phù hợp choApp có nhu cầu data phức tạpFetching đơ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 card
import { 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.jsx

Bà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 APIGlobal state cập nhật thấp (theme, auth, locale)
ZustandShared state không prop drilling và không vấn đề re-render
React Hook Form + ZodForms với validation, không quản lý state thủ công
TanStack QueryServer state — caching, background refetching, mutations
Tailwind CSSStyling không rời khỏi JSX
shadcn/uiThư 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àiTiêu ĐềTrạng Thái
RN-1React.js & Next.js Roadmap✅ Hoàn thành
RN-2Phase 1: Nền Tảng React✅ Hoàn thành
RN-3Phase 2: Hệ Sinh Thái React✅ Bạn đang ở đây
RN-4Phase 3: Next.js Full-StackSắp Ra Mắt
RN-5Deep Dive: Thành Thạo React HooksSắp Ra Mắt
RN-6Deep Dive: State ManagementSắp Ra Mắt
RN-7Deep Dive: App Router & Server ComponentsSắp Ra Mắt
RN-8Deep Dive: Các Pattern Data FetchingSắp Ra Mắt
RN-9Deep Dive: Tối Ưu Hiệu NăngSắp Ra Mắt
RN-10Deep Dive: Authentication trong Next.jsSắp Ra Mắt
RN-11Deep Dive: Testing React & Next.jsSắp Ra Mắt
RN-12Deep Dive: Deploy Ứng Dụng Next.jsSắ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.