0

ReactJS từ Cơ bản đến Nâng cao: Tổng hợp kiến thức cần biết

ReactJS từ Cơ bản đến Nâng cao: Tổng hợp kiến thức cần biết

Bài viết tổng hợp kiến thức về ReactJS — thư viện xây dựng giao diện người dùng phổ biến nhất hiện nay — theo lộ trình từ cơ bản đến nâng cao, kèm ví dụ code minh họa.

Mục lục

  1. Giới thiệu chung về React
  2. Cài đặt và khởi tạo dự án
  3. JSX
  4. Component và Props
  5. State và useState
  6. Vòng đời Component và useEffect
  7. Xử lý sự kiện
  8. Render danh sách và Conditional Rendering
  9. Form và dữ liệu đầu vào
  10. Giao tiếp giữa các Component
  11. Context API
  12. Các Hook quan trọng khác
  13. Custom Hook
  14. React Router
  15. Quản lý State toàn cục nâng cao
  16. Gọi API và xử lý dữ liệu bất đồng bộ
  17. Tối ưu hiệu năng
  18. Error Boundary
  19. Testing trong React
  20. TypeScript với React
  21. Server-side Rendering và Next.js
  22. Tổ chức project lớn
  23. Best Practice
  24. Tài liệu tham khảo

1. Giới thiệu chung về React

React là thư viện JavaScript mã nguồn mở dùng để xây dựng giao diện người dùng (UI), được Facebook (nay là Meta) phát triển và phát hành năm 2013. Đặc điểm nổi bật:

  • Component-based: giao diện được chia thành các component độc lập, tái sử dụng được.
  • Virtual DOM: React tạo một bản sao ảo của DOM trong bộ nhớ, so sánh (diffing) và chỉ cập nhật những phần thay đổi thực sự lên DOM thật, giúp tăng hiệu năng.
  • Unidirectional data flow: dữ liệu chảy một chiều từ component cha xuống component con thông qua props, giúp dễ kiểm soát và debug hơn.
  • Declarative: mô tả giao diện nên trông như thế nào dựa trên state, thay vì viết từng bước thao tác DOM.
  • Hệ sinh thái lớn: React Router, Redux, React Query, Next.js, và rất nhiều thư viện UI (MUI, Ant Design, Chakra UI...).

2. Cài đặt và khởi tạo dự án

Với Vite (khuyến nghị hiện nay, nhanh hơn Create React App)

npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev

Với Next.js (nếu cần SSR/SSG)

npx create-next-app@latest my-app

3. JSX

JSX (JavaScript XML) là cú pháp mở rộng cho phép viết HTML-like code trong JavaScript.

function XinChao() {
  const ten = "Việt Nam";
  return (
    <div className="container">
      <h1>Xin chào, {ten}!</h1>
      {1 + 1 === 2 && <p>Toán học vẫn đúng</p>}
    </div>
  );
}

Lưu ý: JSX dùng className thay vì class, và mọi biểu thức JavaScript được nhúng trong dấu {}.

4. Component và Props

Function Component (chuẩn hiện đại)

function TheSinhVien({ ten, mssv }) {
  return (
    <div className="the-sinh-vien">
      <h3>{ten}</h3>
      <p>MSSV: {mssv}</p>
    </div>
  );
}

// Sử dụng
<TheSinhVien ten="Nguyễn Văn An" mssv="SV001" />

Props mặc định và children

function Nut({ children, mauSac = "xanh" }) {
  return <button className={`nut nut-${mauSac}`}>{children}</button>;
}

<Nut mauSac="do">Xóa</Nut>

propsread-only — component con không được phép thay đổi trực tiếp props nhận từ component cha.

5. State và useState

import { useState } from "react";

function BoDemSo() {
  const [soDem, setSoDem] = useState(0);

  return (
    <div>
      <p>Số hiện tại: {soDem}</p>
      <button onClick={() => setSoDem(soDem + 1)}>Tăng</button>
      <button onClick={() => setSoDem((truoc) => truoc - 1)}>Giảm</button>
    </div>
  );
}

Lưu ý quan trọng: state là bất biến (immutable) — không sửa trực tiếp mà phải dùng hàm set để React biết cần re-render.

// Sai - mutate trực tiếp mảng
danhSach.push(item);
setDanhSach(danhSach);

// Đúng - tạo mảng mới
setDanhSach([...danhSach, item]);

6. Vòng đời Component và useEffect

import { useEffect, useState } from "react";

function DongHo() {
  const [thoiGian, setThoiGian] = useState(new Date());

  useEffect(() => {
    const timer = setInterval(() => setThoiGian(new Date()), 1000);

    // Cleanup function - chạy khi component unmount hoặc trước lần effect tiếp theo
    return () => clearInterval(timer);
  }, []); // mảng dependency rỗng = chỉ chạy 1 lần khi mount

  return <p>{thoiGian.toLocaleTimeString()}</p>;
}
Mảng dependency Thời điểm chạy
Không có (bỏ trống) Sau mỗi lần render
[] Chỉ 1 lần sau khi mount
[bien1, bien2] Khi bien1 hoặc bien2 thay đổi

7. Xử lý sự kiện

function FormDangKy() {
  const xuLyClick = (e) => {
    e.preventDefault();
    console.log("Đã click");
  };

  return <button onClick={xuLyClick}>Đăng ký</button>;
}

Sự kiện trong React dùng camelCase (onClick, onChange) chứ không phải lowercase như HTML thuần, và nhận vào SyntheticEvent (được React chuẩn hóa để hoạt động nhất quán trên mọi trình duyệt).

8. Render danh sách và Conditional Rendering

function DanhSachSinhVien({ danhSach }) {
  if (danhSach.length === 0) {
    return <p>Chưa có sinh viên nào</p>;
  }

  return (
    <ul>
      {danhSach.map((sv) => (
        <li key={sv.id}>{sv.ten}</li>
      ))}
    </ul>
  );
}

Quan trọng: luôn dùng key duy nhất (thường là id từ dữ liệu) khi render list, tuyệt đối không dùng index của mảng làm key nếu danh sách có thể thay đổi thứ tự/thêm/xóa phần tử — vì sẽ gây lỗi re-render sai hoặc mất state.

{daDangNhap ? <TrangChu /> : <TrangDangNhap />}
{coLoi && <ThongBaoLoi message={loi} />}

9. Form và dữ liệu đầu vào

Controlled Component (khuyến nghị)

function FormLienHe() {
  const [ten, setTen] = useState("");
  const [email, setEmail] = useState("");

  const xuLySubmit = (e) => {
    e.preventDefault();
    console.log({ ten, email });
  };

  return (
    <form onSubmit={xuLySubmit}>
      <input value={ten} onChange={(e) => setTen(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <button type="submit">Gửi</button>
    </form>
  );
}

Trong controlled component, giá trị input luôn được đồng bộ với state của React (nguồn dữ liệu duy nhất — "single source of truth"), khác với uncontrolled component (dùng ref để lấy giá trị DOM trực tiếp).

10. Giao tiếp giữa các Component

Cha → Con: qua props

Con → Cha: qua callback function truyền xuống từ props

function ComponentCha() {
  const [tin, setTin] = useState("");

  return <ComponentCon onGuiTin={(giaTri) => setTin(giaTri)} />;
}

function ComponentCon({ onGuiTin }) {
  return <button onClick={() => onGuiTin("Xin chào từ con")}>Gửi</button>;
}

Prop drilling và cách khắc phục

Khi phải truyền props qua nhiều tầng component trung gian không thực sự cần dùng đến nó, đó gọi là prop drilling — giải pháp là dùng Context API hoặc thư viện quản lý state toàn cục (mục 11, 15).

11. Context API

import { createContext, useContext, useState } from "react";

const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  return (
    <AuthContext.Provider value={{ user, setUser }}>
      {children}
    </AuthContext.Provider>
  );
}

function ThongTinUser() {
  const { user } = useContext(AuthContext);
  return <p>Xin chào, {user?.ten ?? "khách"}</p>;
}

// Bọc ngoài cùng
<AuthProvider>
  <App />
</AuthProvider>

Context phù hợp cho dữ liệu ít thay đổi và dùng chung toàn app (theme, ngôn ngữ, thông tin user đăng nhập). Nếu dữ liệu thay đổi liên tục với tần suất cao, Context có thể gây re-render không cần thiết cho toàn bộ cây component tiêu thụ nó — nên cân nhắc thư viện quản lý state chuyên dụng.

12. Các Hook quan trọng khác

useRef

import { useRef } from "react";

function OFocus() {
  const inputRef = useRef(null);

  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Focus vào ô nhập</button>
    </>
  );
}

useRef giữ giá trị không gây re-render khi thay đổi, thường dùng để truy cập trực tiếp DOM element hoặc lưu giá trị "mutable" giữa các lần render.

useMemo và useCallback

import { useMemo, useCallback } from "react";

function DanhSachLoc({ danhSach, tuKhoa }) {
  const ketQuaLoc = useMemo(
    () => danhSach.filter((item) => item.includes(tuKhoa)),
    [danhSach, tuKhoa]
  );

  const xuLyClick = useCallback(() => {
    console.log("Đã click, không tạo lại hàm mỗi lần render");
  }, []);

  return <ul>{ketQuaLoc.map((item) => <li key={item}>{item}</li>)}</ul>;
}
  • useMemo: ghi nhớ kết quả tính toán, tránh tính lại nếu dependency không đổi.
  • useCallback: ghi nhớ tham chiếu hàm, hữu ích khi truyền callback xuống component con được bọc React.memo.

useReducer

import { useReducer } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "tang":
      return { soDem: state.soDem + 1 };
    case "giam":
      return { soDem: state.soDem - 1 };
    default:
      throw new Error("Hành động không hợp lệ");
  }
}

function BoDem() {
  const [state, dispatch] = useReducer(reducer, { soDem: 0 });
  return (
    <>
      <p>{state.soDem}</p>
      <button onClick={() => dispatch({ type: "tang" })}>Tăng</button>
    </>
  );
}

useReducer phù hợp khi logic cập nhật state phức tạp, có nhiều hành động khác nhau — dễ đọc và test hơn so với nhiều useState rời rạc.

13. Custom Hook

import { useState, useEffect } from "react";

function useFetch(url) {
  const [data, setData] = useState(null);
  const [dangTai, setDangTai] = useState(true);
  const [loi, setLoi] = useState(null);

  useEffect(() => {
    let huy = false;
    setDangTai(true);
    fetch(url)
      .then((res) => res.json())
      .then((data) => !huy && setData(data))
      .catch((err) => !huy && setLoi(err))
      .finally(() => !huy && setDangTai(false));

    return () => { huy = true; }; // tránh set state sau khi component unmount
  }, [url]);

  return { data, dangTai, loi };
}

// Sử dụng
function DanhSachGiaSu() {
  const { data, dangTai, loi } = useFetch("/api/gia-su");
  if (dangTai) return <p>Đang tải...</p>;
  if (loi) return <p>Lỗi: {loi.message}</p>;
  return <ul>{data.map((gs) => <li key={gs.id}>{gs.ten}</li>)}</ul>;
}

Custom Hook là cách tốt nhất để tái sử dụng logic có state giữa nhiều component, quy ước đặt tên phải bắt đầu bằng use.

14. React Router

npm install react-router-dom
import { BrowserRouter, Routes, Route, Link, useNavigate, useParams } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Trang chủ</Link>
        <Link to="/gia-su">Gia sư</Link>
      </nav>
      <Routes>
        <Route path="/" element={<TrangChu />} />
        <Route path="/gia-su" element={<DanhSachGiaSu />} />
        <Route path="/gia-su/:id" element={<ChiTietGiaSu />} />
        <Route path="*" element={<Trang404 />} />
      </Routes>
    </BrowserRouter>
  );
}

function ChiTietGiaSu() {
  const { id } = useParams();
  const navigate = useNavigate();

  return (
    <div>
      <p>Chi tiết gia sư ID: {id}</p>
      <button onClick={() => navigate(-1)}>Quay lại</button>
    </div>
  );
}

Protected Route (bảo vệ trang cần đăng nhập)

function RouteBaoVe({ children }) {
  const { user } = useContext(AuthContext);
  if (!user) return <Navigate to="/dang-nhap" replace />;
  return children;
}

<Route path="/quan-ly" element={<RouteBaoVe><TrangQuanLy /></RouteBaoVe>} />

15. Quản lý State toàn cục nâng cao

Khi ứng dụng lớn và Context API không còn đủ hiệu quả (do vấn đề re-render hoặc logic phức tạp), có thể dùng các thư viện chuyên dụng:

Thư viện Đặc điểm
Redux Toolkit Chuẩn công nghiệp lâu đời, predictable state container, hỗ trợ DevTools mạnh
Zustand Nhẹ, API đơn giản, ít boilerplate, không cần Provider bọc
Jotai / Recoil Quản lý state dạng atom, tối ưu cho re-render ở mức chi tiết
TanStack Query (React Query) Chuyên biệt cho state đến từ server (cache, refetch, đồng bộ)

Ví dụ với Zustand

import { create } from "zustand";

const useAuthStore = create((set) => ({
  user: null,
  dangNhap: (user) => set({ user }),
  dangXuat: () => set({ user: null }),
}));

function ThongTinUser() {
  const user = useAuthStore((state) => state.user);
  return <p>Xin chào, {user?.ten ?? "khách"}</p>;
}

16. Gọi API và xử lý dữ liệu bất đồng bộ

Với TanStack Query (khuyến nghị cho state đến từ server)

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import axios from "axios";

function DanhSachGiaSu() {
  const { data, isLoading, error } = useQuery({
    queryKey: ["gia-su"],
    queryFn: () => axios.get("/api/gia-su").then((res) => res.data),
  });

  if (isLoading) return <p>Đang tải...</p>;
  if (error) return <p>Có lỗi xảy ra</p>;
  return <ul>{data.map((gs) => <li key={gs.id}>{gs.ten}</li>)}</ul>;
}

function TaoGiaSu() {
  const queryClient = useQueryClient();
  const mutation = useMutation({
    mutationFn: (giaSuMoi) => axios.post("/api/gia-su", giaSuMoi),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ["gia-su"] }),
  });

  return <button onClick={() => mutation.mutate({ ten: "Mới" })}>Tạo</button>;
}

React Query tự động xử lý caching, refetch khi focus lại tab, retry khi lỗi, và tránh phải tự quản lý dangTai/loi thủ công như khi dùng useEffect + fetch truyền thống.

17. Tối ưu hiệu năng

React.memo — tránh re-render component con không cần thiết

const TheSinhVien = React.memo(function TheSinhVien({ ten }) {
  console.log("Render lại thẻ:", ten);
  return <div>{ten}</div>;
});

React.memo chỉ re-render component khi props thay đổi (so sánh shallow) — cần kết hợp useCallback/useMemo ở component cha để tránh tạo prop mới (hàm/object) mỗi lần render làm mất tác dụng của memo.

Code Splitting với lazy loading

import { lazy, Suspense } from "react";

const TrangQuanLy = lazy(() => import("./TrangQuanLy"));

<Suspense fallback={<p>Đang tải trang...</p>}>
  <TrangQuanLy />
</Suspense>

Virtualization cho danh sách dài

Với danh sách hàng ngàn phần tử, dùng thư viện như react-window hoặc react-virtualized để chỉ render các phần tử đang hiển thị trong viewport, tránh render toàn bộ DOM node cùng lúc.

18. Error Boundary

class ErrorBoundary extends React.Component {
  state = { coLoi: false };

  static getDerivedStateFromError() {
    return { coLoi: true };
  }

  componentDidCatch(error, info) {
    console.error("Lỗi bắt được:", error, info);
  }

  render() {
    if (this.state.coLoi) {
      return <h2>Đã có lỗi xảy ra, vui lòng thử lại.</h2>;
    }
    return this.props.children;
  }
}

<ErrorBoundary>
  <App />
</ErrorBoundary>

Error Boundary phải là class component (React chưa hỗ trợ hook tương đương cho componentDidCatch), dùng để bắt lỗi runtime trong cây component con và hiển thị fallback UI thay vì để trắng trang toàn bộ ứng dụng.

19. Testing trong React

npm install -D vitest @testing-library/react @testing-library/jest-dom
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import BoDemSo from "./BoDemSo";

describe("BoDemSo", () => {
  it("tăng số đếm khi click nút Tăng", () => {
    render(<BoDemSo />);
    const nut = screen.getByText("Tăng");
    fireEvent.click(nut);
    expect(screen.getByText("Số hiện tại: 1")).toBeInTheDocument();
  });
});

React Testing Library khuyến khích test theo cách người dùng thực sự tương tác (tìm phần tử qua text, role, label) thay vì test chi tiết cài đặt nội bộ component, giúp test bền vững hơn khi refactor code.

20. TypeScript với React

interface SinhVien {
  id: number;
  ten: string;
  mssv: string;
}

interface Props {
  sinhVien: SinhVien;
  onXoa: (id: number) => void;
}

function TheSinhVien({ sinhVien, onXoa }: Props) {
  return (
    <div>
      <p>{sinhVien.ten}</p>
      <button onClick={() => onXoa(sinhVien.id)}>Xóa</button>
    </div>
  );
}

const [danhSach, setDanhSach] = useState<SinhVien[]>([]);

TypeScript giúp bắt lỗi kiểu dữ liệu ngay lúc code thay vì runtime, đặc biệt hữu ích khi project lớn, nhiều người cùng phát triển.

21. Server-side Rendering và Next.js

React thuần chạy hoàn toàn phía client (CSR - Client-side Rendering), có thể gây chậm SEO và First Contentful Paint. Next.js giải quyết vấn đề này bằng:

  • SSR (Server-side Rendering): render HTML ở server cho mỗi request.
  • SSG (Static Site Generation): build sẵn HTML tĩnh lúc build time.
  • ISR (Incremental Static Regeneration): kết hợp SSG nhưng tự động cập nhật lại theo chu kỳ.
  • React Server Components (App Router): component chạy hoàn toàn ở server, giảm JS gửi về client.
// app/gia-su/page.tsx (Next.js App Router)
async function TrangGiaSu() {
  const res = await fetch("https://api.example.com/gia-su");
  const danhSach = await res.json();
  return <ul>{danhSach.map((gs) => <li key={gs.id}>{gs.ten}</li>)}</ul>;
}

export default TrangGiaSu;

22. Tổ chức project lớn

src/
├── components/       # Component dùng chung (Button, Modal, Input...)
├── pages/            # Các trang tương ứng route
├── features/         # Chia theo tính năng (auth, gia-su, dang-ky-lich...)
│   └── gia-su/
│       ├── components/
│       ├── hooks/
│       ├── api.js
│       └── index.js
├── hooks/            # Custom hook dùng chung
├── context/          # Context Provider
├── services/         # Gọi API, axios instance
├── utils/            # Hàm tiện ích
└── App.jsx

Cách tổ chức theo feature (thay vì theo loại file) giúp project dễ mở rộng, mỗi tính năng độc lập, dễ tìm code liên quan khi cần sửa.

23. Best Practice

  • Ưu tiên function component + hook, hạn chế viết class component (trừ Error Boundary).
  • Giữ component nhỏ, đơn nhiệm — một component chỉ nên làm một việc rõ ràng.
  • Tách logic phức tạp ra custom hook, giữ component chỉ lo phần render.
  • Luôn dùng key ổn định và duy nhất khi render list, tránh dùng index.
  • Không gọi Hook bên trong điều kiện, vòng lặp hay hàm lồng — Hook luôn phải gọi ở top-level của component.
  • Dùng React.memo, useMemo, useCallback có chọn lọc — không tối ưu sớm (premature optimization) khi chưa đo hiệu năng thực tế.
  • Đặt tên props/state rõ nghĩa, tránh viết tắt khó hiểu.
  • Sử dụng ESLint (kèm eslint-plugin-react-hooks) để bắt lỗi vi phạm quy tắc Hook và các lỗi phổ biến khác.

24. Tài liệu tham khảo


Bài viết được tổng hợp nhằm mục đích chia sẻ kiến thức, phù hợp làm tài liệu ôn tập hoặc tra cứu nhanh khi xây dựng giao diện với ReactJS.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí