Nội dung bài viết
- Render phase là gì ?
- Commit phase là gì ?
- Một Rendering Cycle đầy đủ diễn ra như thế nào?
- Từ setState đến lúc UI đổi: chuyện gì thực sự xảy ra?
- Bước 1: Event xảy ra
- Bước 2: setCount(count + 1) được gọi
- Bước 3: React chuẩn bị một lần render mới
- Bước 4: React gọi lại Counter()
- Bước 5: React so sánh với UI cũ
- Bước 6: React commit
- Bước 7: Browser hiển thị UI mới
- Vì sao console.log sau setState vẫn là giá trị cũ?
- Vì sao gọi setCount(count + 1) ba lần nhưng chỉ tăng một?
- Re-render không có nghĩa DOM chắc chắn thay đổi
- useEffect nằm ở đâu trong chu trình này?
- Kết luận
Gần đây mình đang ôn lại các kiến thức chuyên môn để đi phỏng vấn, đọc kỹ lại docs vì giờ React docs đã cập nhật lại khá nhiều. Đôi khi đi làm thì biết nó là như vậy nhưng để hiểu ngọn ngành, giải thích luồng hoạt động thì có khi giải thích không được chính xác hoàn toàn. Một trong số kiến thức React mà mình nghĩ nhiều người(trong đó có mình) còn chưa thật sự hiểu hoàn toàn đó chính là Rendering Cycle.
Khi làm việc hoặc đi phỏng vấn với React chắc chắn các bạn sẽ không ít lần thắc mắc hoặc được nhà tuyển dụng hỏi như là:
- Tại sao gọi
setStaterồi nhưng giá trị chưa đổi ngay - Tại sao
console.logvẫn in ra state cũ - Component re-render nhưng nhìn giao diện chẳng thấy thay đổi gì
- Nghe mọi người nói về render, re-render, effect, commit… nhưng càng nghe càng thấy rối não
Nếu các bạn từng có cảm giác như vậy, thì khả năng cao vấn đề không nằm ở việc bạn code chưa đủ nhiều mà là các bạn chưa tách bạch được hai khái niệm rất quan trọng trong React đó chính là render phase và commit phase.
Nhiều người nghĩ rằng:
React re-render nghĩa là giao diện(UI) được cập nhật ngay.
Nhưng đó không phải là cách React vận hành.
Muốn hiểu đúng về Rendering Cycle, các bạn cần nắm được:
- Render là lúc React tính toán UI mới trông như thế nào
- Commit mới là lúc React áp dụng những thay đổi cần thiết lên DOM thật
Sự khác nhau này nghe có vẻ đơn giản, nhưng lại là gốc rễ của rất nhiều thứ khác trong React mà các bạn hay thắc mắc:
- Vì sao
setStatekhông đổi giá trị ngay trong dòng code hiện tại - Vì sao state được xem như một snapshot
- Vì sao
useEffectchạy sau - Vì sao component render lại chưa chắc DOM đã đổi
- Vì sao tối ưu React không đơn giản là thêm
useMemohayReact.memo
Trong bài viết này, mình sẽ đi từ bản chất đến ví dụ thực tế để giúp các bạn có thể hiểu rõ được render khác commit như thế nào.

Render phase là gì ?
Trong React, render là: React gọi component function để tính xem UI mới nên trông như thế nào. Tức là ở bước này, React đang tính toán, chưa phải thay đổi giao diện thật. Mình có đoạn code đơn giản dưới đây:
function Counter() {
const [count, setCount] = useState(0);
return <h1>{count}</h1>;
}
Khi count = 0, component trả về:
<h1>0</h1>
Khi count = 1, React gọi lại Counter() và nhận được:
<h1>1</h1>
Đó chính là render. Ở đây có một điểm rất quan trọng mà các bạn nên chú ý:
Render là lúc React tạo ra một “phiên bản UI mới” trên mặt logic, chưa phải lúc sửa DOM(Document Object Model) thật.
Bạn có thể hình dung mỗi lần render là một lần React chụp lại một snapshot của UI dựa trên:
- State hiện tại
- Props hiện tại
- Context hiện tại
Snapshot này trả lời câu hỏi:
Với dữ liệu hiện tại, UI nên trông như thế nào?
Commit phase là gì ?
Sau khi React đã render xong và biết UI mới trông ra sao, nó mới đi sang bước tiếp theo chính là commit.
Cụ thể, commit là lúc React:
- Áp dụng những thay đổi cần thiết lên DOM thật
- Cập nhật ref
- Chuẩn bị cho các effects(useEffects) chạy theo timing phù hợp
Ví dụ, nếu DOM thật trước đó là:
<h1>0</h1>
và sau render, React biết UI mới phải là:
<h1>1</h1>
thì commit là lúc React thực sự cập nhật text node từ 0 thành 1.
Đây mới là thời điểm thay đổi chạm tới giao diện thật mà người dùng có thể nhìn thấy.

Một Rendering Cycle đầy đủ diễn ra như thế nào?
Khi có một thay đổi xảy ra, React không nhảy thẳng tới chuyện cập nhật giao diện ngay lập tức. Thay vào đó, nó đi qua một chu trình. Các bạn có thể hình dung flow cơ bản như sau:
Event xảy ra → React nhận update → React render lại để tính UI mới → React so sánh với UI cũ → React commit phần thay đổi lên DOM → Browser hiển thị giao diện mới
Nếu muốn chi tiết hơn một chút, hãy đọc như sau:
- Có update xảy ra
Ví dụ: user click, input đổi, props đổi, context đổi - React nhận yêu cầu update
Thường là từsetState, parent re-render, context update… - React render
React gọi lại component function để tính snapshot UI mới - React so sánh kết quả mới với kết quả cũ
Để biết chỗ nào thực sự cần cập nhật - React commit
Cập nhật DOM thật với phần thay đổi cần thiết - Browser paint
Trình duyệt hiển thị giao diện mới - Effects chạy
Ví dụuseEffectchạy sau khi UI đã được commit
Đây chính là bức tranh tổng thể của Rendering Cycle mà bạn cần giữ trong đầu.
Từ setState đến lúc UI đổi: chuyện gì thực sự xảy ra?
Các bạn hãy xem ví dụ đơn giản dưới đây:
function Counter() {
const [count, setCount] = useState(0);
console.log("render", count);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Giả sử ban đầu count = 0.
Màn hình hiển thị:
Count: 0
Khi user click button, chuyện gì diễn ra?
Bước 1: Event xảy ra
Người dùng click button.
Bước 2: setCount(count + 1) được gọi
Lúc này React nhận được một yêu cầu update.
Bước 3: React chuẩn bị một lần render mới
React lên lịch để render lại component Counter.
Bước 4: React gọi lại Counter()
Bây giờ React tính toán UI mới. Với state mới, nó kết luận rằng UI nên là:
<button>Count: 1</button>
Đây là render.
Bước 5: React so sánh với UI cũ
React thấy rằng chỉ có phần text thay đổi từ 0 sang 1.
Bước 6: React commit
React cập nhật text node tương ứng trong DOM thật.
Bước 7: Browser hiển thị UI mới
Lúc này người dùng mới nhìn thấy Count: 1.
Điểm cần chốt ở đây là:
setStatekhông phải “đổi biến ngay”.setStatelà “báo cho React biết rằng cần một lần render mới”.
Đây là chỗ rất nhỏ nhưng cực kỳ quan trọng.
Vì sao console.log sau setState vẫn là giá trị cũ?
Đây là một trong những câu hỏi phổ biến nhất khi học React.
Ví dụ:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count);
};
return <button onClick={handleClick}>Count: {count}</button>;
}
Nhiều người mong rằng click xong, console sẽ in ra 1.
Nhưng thực tế thường vẫn là 0.
Lý do là vì trong lúc handleClick đang chạy, bạn vẫn đang đứng trong ngữ cảnh của lần render hiện tại. Mà trong lần render đó, count là 0.
Lệnh setCount(count + 1) không làm cho biến count trong đoạn code hiện tại thay đổi ngay lập tức. Nó chỉ yêu cầu React tạo ra một lần render mới sau đó.
Một cách hình dung rất dễ nhớ:
- Mỗi lần render là một bức ảnh chụp
- Trong bức ảnh hiện tại,
count = 0 - Bạn gọi
setCount(...) - React nói: “ok, tôi sẽ chuẩn bị bức ảnh mới”
- Nhưng ngay lúc này, bạn vẫn đang ở trong bức ảnh cũ
Vì vậy console.log(count) vẫn là 0.
Cách hiểu này cực kỳ mạnh, vì nó dẫn bạn đến mental model đúng:
State trong mỗi lần render là một snapshot.

Vì sao gọi setCount(count + 1) ba lần nhưng chỉ tăng một?
Hãy xem ví dụ này:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
return <button onClick={handleClick}>Count: {count}</button>;
}
Nhiều người nghĩ click một cái sẽ tăng từ 0 lên 3.
Nhưng thực tế thường chỉ lên 1.
Lý do là vì trong lần render hiện tại, count vẫn là 0. Nên cả ba dòng này thực chất đều tương đương với:
setCount(0 + 1); setCount(0 + 1); setCount(0 + 1);
Kết quả cuối cùng vẫn là 1.
Nếu bạn muốn cộng dồn đúng, hãy dùng function updater:
const handleClick = () => {
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
};
Lúc này mỗi lần update sẽ lấy giá trị mới nhất trong chuỗi update, nên kết quả sẽ là 3.
Ví dụ này giúp bạn thấy rất rõ rằng:
- state không đổi ngay trong đoạn code hiện tại
- React xử lý update theo queue
- snapshot của lần render hiện tại rất quan trọng
Re-render không có nghĩa DOM chắc chắn thay đổi
Ví dụ:
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increase</button>
<Child />
</div>
);
}
function Child() {
console.log("Child render");
return <p>Hello</p>;
}
Mỗi lần Parent render lại, Child cũng có thể bị gọi lại.
Nhưng Child luôn trả về cùng một kết quả:
<p>Hello</p>
Vậy điều gì xảy ra?
Childcó thể render lại- nhưng DOM thật của
Childchưa chắc phải thay đổi
Đây là chỗ bạn nên tách bạch hai loại chi phí:
- Render cost: chi phí React gọi lại component và tính toán UI
- DOM / commit cost: chi phí thay đổi DOM thật
Hai loại chi phí này không giống nhau.
Điều này rất quan trọng trong performance. Nhiều người thấy console.log("render") xuất hiện nhiều lần là hoảng, rồi vội vàng thêm memo, useMemo, useCallback khắp nơi. Nhưng nếu component render lại rất nhẹ và DOM gần như không đổi, thì chưa chắc đó là bottleneck(nút chai) thực sự.
useEffect nằm ở đâu trong chu trình này?
Hãy xem ví dụ:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
return <div>{user ? user.name : "Loading..."}</div>;
}
Lần đầu component mount, chuyện gì xảy ra?
- React render với
user = null - React commit UI là
Loading... - Browser hiển thị
Loading... - Sau đó
useEffectmới chạy - Fetch xong,
setUser(data)được gọi - React bắt đầu một rendering cycle mới
- Render lại với dữ liệu user
- Commit tên user thật lên DOM
Điểm quan trọng là:
useEffectkhông chạy trong render.
Nó chạy sau khi React đã commit UI.
Kết luận
Rất nhiều người học React nói về render, re-render, state update, effect… nhưng trong đầu vẫn đang gộp tất cả thành một quá trình khá mơ hồ.
Khi bạn tách rõ được:
- Render là lúc React tính xem UI mới nên trông như thế nào
- Commit là lúc React áp dụng những thay đổi cần thiết lên DOM thật
thì rất nhiều thứ trong React sẽ trở nên sáng tỏ hơn:
- Vì sao
setStatekhông cập nhật ngay tại chỗ - Vì sao state trong mỗi lần render là snapshot
- Vì sao
useEffectchạy sau - Vì sao component render lại chưa chắc DOM đổi
- Vì sao performance cần được nhìn đúng bản chất
Hiểu render khác commit không chỉ giúp bạn “biết thêm lý thuyết”. Nó giúp bạn xây được mental model đúng về React.
Đây là một trong những bài viết trong series React Fundamentals. Sắp tới mình sẽ cố gắng viết thêm nhiều bài nữa. Hi vọng các bạn ủng hộ. Đừng quên để lại bình luận để mình có thêm động lực viết bài nha.


![[Quan trọng] - Hướng dẫn mua và thanh toán khóa học Evondev mới nhất blog thumb](https://evondev.com/wp-content/uploads/2023/12/blog-thumb.png)