SydexaSydexa

React Nâng Cao

Trước khi React chạyJavaScript cho React
TypeScript Cơ BảnType System & GenericUtility Types & tsconfig
ReactJS Căn Bản
Reconciliation & FiberFiber Deep Dive
React HooksOptimization & Utility
React Hook Form & ZodAdvanced Form Patterns
Web Vitals & DevToolsPerformance Playbooks
TanStack VirtualTanStack Virtual (P2)
TanStack QueryTanStack Query (P2)
ZustandZustand (P2)Fiber ↔ External Store
Bảo mật frontendBảo mật frontend (2)
Trang chủKhoá họcReact Nâng Cao
3 lỗi React không nằm ở React
Mutation không re-render, closure giữ state cũ, console.log(state) ra giá trị cũ. Bài thử này cho bạn thấy gốc JavaScript phía sau 3 lỗi React phổ biến.

3 lỗi React không nằm ở React

0:00/0:00
Cách học bài này:

Mỗi phần đi theo một nhịp: bug React trước, JavaScript root cause sau, rồi pattern sửa ngay. Nếu bạn từng thấy React “khó đoán”, đây là lúc bóc nó ra thành những cơ chế nhỏ hơn.

Warm-up

1.React thường dùng tín hiệu nào để biết object/array state đã đổi?
1/1
1/1

Lỗi 1: State đổi rồi, sao UI không đổi?

Bug kinh điển: bạn sửa field trong object, gọi setter, không có lỗi đỏ, nhưng màn hình vẫn đứng im. React không đọc sâu từng property để kiểm tra bạn sửa gì. React hỏi một câu ngắn hơn nhiều: reference có đổi không?

0:00/0:00

Mutation vs Immutability

Mutation0x001name: 'Minh'age: 25age: 26 ← sửa tại chỗuser.age = 26; setUser(user)Reference vẫn 0x001!0x001 === 0x001 → Cùng ref!Không thay đổi → bỏ qua!Không re-renderImmutability0x001name: 'Minh'age: 25{ ...user, age: 26 }▼0x002NEWname: 'Minh'age: 260x001 !== 0x002 → Khác ref!Có thay đổi → cập nhật UI!Re-render!

So sánh cùng một state update theo 2 cách: Mutation vs Immutability

Root cause:

const không làm object bất biến. Nó chỉ khóa biến đang trỏ tới object đó. Nếu bạn mutation rồi truyền lại đúng object cũ, React thấy reference cũ và có thể bỏ qua render.

Pattern React cần:

Khi update state dạng object/array, hãy tạo reference mới: setUser({ ...user, name: "An" }). Đây không phải style cho đẹp, mà là tín hiệu để React biết dữ liệu đã đổi.

Quick Quiz

1.Đoạn update nào dễ khiến React không re-render?
12
user.name = "An";
setUser(user);
1/1
1/1

Lỗi 2: Callback cứ nhớ state cũ

Bạn tạo interval khi count = 0. Sau đó UI tăng lên 5, nhưng interval vẫn log 0. Không phải React bị lạc. Function của bạn đang giữ một snapshot của render cũ.

0:00/0:00

Stale Closure vs Functional Update

count0 // handleClick()setCount(count + 1)← count = 0 (closure)setCount(count + 1)← count vẫn = 0!setCount(count + 1)← count vẫn = 0!setCount(prev => prev + 1)← prev=0 → 1setCount(prev => prev + 1)← prev=1 → 2setCount(prev => prev + 1)← prev=2 → 3

 

Closure trong React:

Mỗi render tạo ra một phiên bản mới của biến và callback. Nếu callback được giữ lại bởi setInterval, event listener, promise hoặc effect thiếu dependency, nó có thể tiếp tục nhìn thấy state từ render đã qua.

Cách sửa:

Với update dựa trên giá trị trước đó, dùng functional update: setCount((c) => c + 1). Với effect đọc biến bên ngoài, dependency array phải nói thật về những gì callback đang dùng.

Quick Quiz

1.Vì sao interval trong useEffect có thể log mãi count cũ?
1/1
1/1

CRUD state: đừng sửa mảng, hãy tạo mảng mới

Sau khi hiểu reference, các method array trong React không còn là mẹo cú pháp. Chúng là bộ công cụ để tạo dữ liệu mới mà vẫn giữ phần cũ không đổi.

Array CRUD Visualizer - Tương tác

Chế độ

Operations

Toggle
Remove
Clear all completed
Reorder↔

Gợi ý

  • · Bấm Add ở chế độ Mutate - block mới hiện ra nhưng counter không tăng. Vì sao?
  • · Đổi sang Immutable rồi bấm lại - counter tăng, border vàng nhấp nháy.
  • · Toggle ở Mutate: checkbox đổi nhưng React thật sẽ không thấy.
Component<TodoList />
renders:1

todos: Todo[]length: 3

array ref
[0]id:1Học Zustandcompleted: false
[1]id:2Code todo CRUDcompleted: false
[2]id:3Đọc Zustand docscompleted: true
ref cũ (xanh)ref mới (vàng)mutate cảnh báo (đỏ)

Chọn chế độ Mutate / Immutable rồi thực hiện thao tác. Quan sát Render counter và border của từng block.

  • Add - Dùng spread để thêm item: setTodos([...todos, newTodo]).
  • Update - Dùng map để chỉ thay item khớp id, các item còn lại giữ nguyên.
  • Delete - Dùng filter để tạo array mới không chứa item cần xóa.
Mental model:

React thích những thao tác có thể so sánh nhanh. map, filter và spread giúp bạn tạo reference mới ở đúng nơi cần đổi, nhưng vẫn giữ lại reference cũ cho phần không đổi.

Lỗi 3: setState xong log vẫn ra giá trị cũ

Setter của React không giống phép gán biến. Nó đặt một yêu cầu update vào hàng đợi, React gom các update lại rồi render phiên bản tiếp theo. Vì vậy dòng code ngay sau setter vẫn đang đứng trong render hiện tại.

0:00/0:00

Interactive Event Loop

0/6
Codeconsole.log('A');setTimeout(() => { console.log('B');}, 0);console.log('C');Call StackWeb APIsConsoleMicrotask QueueTask Queueasyncdone↻Event Loop

Bấm Next Step để bắt đầu thực thi code

Cách đọc state đúng:

Nếu cần phản ứng sau khi state đổi, đọc nó trong render tiếp theo hoặc trong effect phù hợp. Nếu cần tính state mới từ state cũ, dùng setCount((c) => c + 1) để tránh dựa vào snapshot hiện tại.

Quick Quiz

1.Trong event handler, vì sao log sau setCount vẫn có thể là giá trị cũ?
1/1
1/1

Mini lab: sửa 3 bug React trong 10 phút

Đến đây, bài học không cần thêm định nghĩa. Bạn chỉ cần nhìn bug, đoán root cause, rồi sửa. Đây là nhịp debug thật: reference trước, closure sau, async cuối.

0:00/0:00
Lab 1: Reference:

Chạy thử và để ý dòng same reference?. Nếu cùng reference, React gần như không có tín hiệu rõ ràng để render lại.

reference-lab.js
JavaScript
1234567891011
const prevUser = { id: 1, name: "Nam", role: "Member" };
const nextUser = prevUser;

nextUser.role = "Admin";

console.log("same reference?", Object.is(prevUser, nextUser));
console.log("React sees:", Object.is(prevUser, nextUser) ? "no signal" : "changed");

const fixedUser = { ...prevUser, role: "Owner" };
console.log("fixed same reference?", Object.is(prevUser, fixedUser));
console.log(fixedUser);
⌘+Enter
Lab 2: Closure:

Callback đầu tiên giống callback được tạo từ render cũ. Functional update là cách nói với React: hãy dùng giá trị mới nhất khi xử lý.

closure-lab.js
JavaScript
123456789101112
function createBadIncrement(count) {
  return () => count + 1;
}

let count = 0;
const incrementFromFirstRender = createBadIncrement(count);

count = 10;
console.log("stale result:", incrementFromFirstRender());

const functionalUpdate = (current) => current + 1;
console.log("fresh result:", functionalUpdate(count));
⌘+Enter
Lab 3: React state:

Click cả hai nút. Nút đầu mutation object hiện tại. Nút thứ hai tạo object mới để effect và UI có tín hiệu cập nhật rõ ràng.

react-state-lab.jsx
React
1234567891011121314151617181920212223242526272829303132
import React, { useEffect, useState } from "react";

export default function App() {
  const [user, setUser] = useState({ name: "Nam", score: 1 });
  const [logs, setLogs] = useState([]);

  function mutateBug() {
    user.score += 1;
    setUser(user);
    setLogs((items) => [...items, "Mutation: reference cũ"]);
  }

  function fixedUpdate() {
    setUser((current) => ({ ...current, score: current.score + 1 }));
    setLogs((items) => [...items, "Immutable: reference mới"]);
  }

  useEffect(() => {
    setLogs((items) => [...items, "Render score = " + user.score]);
  }, [user]);

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <h2>{user.name}: {user.score}</h2>
      <button onClick={mutateBug}>Mutation bug</button>
      <button onClick={fixedUpdate} style={{ marginLeft: 8 }}>
        Fix đúng
      </button>
      <pre>{logs.join("\n")}</pre>
    </div>
  );
}
Preview
Nhấn Chạy để xem kết quả
⌘+Enter

Bạn vừa debug React bằng JavaScript internals

Ba bug trong bài này có vẻ khác nhau, nhưng đều về cùng một chuyện: React không đoán ý bạn. React nhìn reference, chạy callback từ render hiện tại, rồi schedule update cho render tiếp theo.

  • Mutation không re-render - Gốc rễ là reference equality, không phải React khó tính.
  • Closure giữ state cũ - Gốc rễ là callback nhớ snapshot của render nơi nó được tạo.
  • Log sau setter ra giá trị cũ - Gốc rễ là state update được schedule, không phải phép gán đồng bộ.

Knowledge Check

1.Khi gặp một bug React không rõ lý do, thứ tự debug nào hợp lý nhất?
1/1
1/1
Trong phần trả phí:

Bạn sẽ đi tiếp từ các root cause này vào render phase vs commit phase, Fiber, Hooks internals, memoization thật sự có lợi khi nào, async race condition, profiling và các pattern state management. Nói cách khác: không chỉ biết React API, mà hiểu được vì sao app React chạy như vậy.

Bài trướcBài tiếp theo