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.
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?
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.
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.
user.name = "An";
setUser(user);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ũ.
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.
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.
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.
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.
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.
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.
Đế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.
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.
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);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ý.
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));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.
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>
);
}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.
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.