Một component React render lại khi state của nó thay đổi, khi component cha render lại, hoặc khi một context nó đọc thay đổi. Render nghĩa là React gọi lại hàm component của bạn để lấy JSX mới; sau đó nó chỉ cập nhật những phần trên trang thực sự thay đổi.
Mở Console bên dưới bản xem trước rồi nhấn nút. Cả ba component đều ghi log, kể cả Title, vốn không nhận prop nào. Nó render vì component cha của nó render.
Điều gì kích hoạt một lần render
React render một component vì đúng những lý do sau:
- Lần render đầu tiên. Ứng dụng khởi chạy, hoặc component xuất hiện trong cây lần đầu.
- State của nó thay đổi. Bạn gọi hàm setter từ
useStatehoặc dispatch từuseReducervới một giá trị mới. - Component cha của nó render. Mặc định, khi một component render, mọi component nó trả về cũng render theo, xuống tận cùng.
- Một context nó đọc thay đổi. Component gọi
useContext(SomeContext)sẽ render khi provider gần nhất truyền mộtvaluemới (xem useContext).
Nhiều người nghĩ component render "vì props của nó thay đổi". Cách hiểu đó đảo ngược nguyên nhân. Props là đối số mà component cha truyền vào khi nó render, nên props mới chỉ có thể đến khi component cha render. Và chỉ riêng việc component cha render là đã đủ: Title ở trên không có props mà vẫn render mỗi lần.
Đặt state về đúng giá trị nó đang có (so sánh bằng Object.is) không làm các component con render. React có thể vẫn gọi riêng component đó một lần trước khi nhận ra không có gì thay đổi, nhưng nó bỏ kết quả đi.
Render và commit
Mỗi lần cập nhật đi qua hai giai đoạn:
- Render. React gọi các component của bạn. Chúng trả về JSX, thực chất chỉ là các object mô tả những gì màn hình cần hiển thị. Chưa có gì trên trang thay đổi, vì vậy render phải thuần (pure): không ghi vào DOM, không gửi request, không thay đổi biến bên ngoài component.
- Commit. React so sánh kết quả mới với kết quả trước và áp dụng các khác biệt vào DOM: nó chỉ thêm, xóa hoặc cập nhật những node đã thay đổi. Sau đó trình duyệt vẽ lại, và tiếp theo React chạy các effect của bạn.
Ví dụ dưới đây render ở mỗi lần nhấn, nhưng React vẫn giữ nguyên phần tử <input>. Một effect chạy sau mỗi lần commit và kiểm tra điều đó.
Gõ gì đó vào ô nhập rồi nhấn vài lần. Đoạn văn bản bạn gõ vẫn còn, vì React không hề thay ô nhập: thay đổi DOM duy nhất sau mỗi lần nhấn là con số bên trong <p>. Log cũng cho thấy thứ tự: render trước, effect chạy sau khi commit.
Virtual DOM, nói đơn giản
"Virtual DOM" là tên gọi phổ biến cho các object mà component của bạn trả về. <p>Items: {count}</p> được biên dịch thành một lời gọi tạo ra object kiểu { type: 'p', props: { children: ['Items: ', 1] } }. Sau một lần render, React duyệt cây object mới song song với cây trước, và ở chỗ nào loại phần tử và vị trí khớp nhau thì nó giữ node DOM hiện có, chỉ cập nhật thuộc tính và văn bản đã thay đổi. Quá trình so sánh này gọi là reconciliation.
Thuật ngữ này không chính xác lắm vì hai lý do. React không giữ một bản sao thứ hai của DOM rồi so sánh hai DOM: nó so sánh các object phần tử với cây component nội bộ của chính nó (cây fiber). Và cùng quá trình đó còn điều khiển cả những nền tảng không có DOM, như React Native. Tài liệu React phần lớn tránh cụm từ này và nói về render và commit. Điều quan trọng trong thực tế là hệ quả: render rẻ hơn nhiều so với thao tác DOM, vì phần lớn lần render chỉ kết thúc bằng một cập nhật DOM nhỏ hoặc không có cập nhật nào.
Cách so sánh này dẫn đến hai quy tắc. Một loại phần tử khác ở cùng vị trí (một <div> bị thay bằng <section>, hoặc ComponentA bằng ComponentB) sẽ hủy cây con cũ cùng state của nó. Và trong danh sách, key cho React biết mục nào là mục nào, để nó có thể di chuyển node thay vì dựng lại.
Chặn những lần render không cần thiết
Phần lớn lần render thừa không tốn gì mà bạn nhận ra được. Khi một lần render thực sự gây chậm, ví dụ một danh sách lớn render lại mỗi lần gõ phím ở chỗ khác trên trang, hãy thử lần lượt các cách sau.
Đưa state xuống thấp hơn
Nếu chỉ một phần nhỏ của màn hình dùng một state, hãy giữ state đó trong một component chỉ bao quanh phần đó. Phiên bản này render lại ProductList ở mỗi lần gõ phím:
export default function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<ProductList />
</>
);
}
Chuyển ô nhập và state của nó vào một component riêng, và danh sách không còn nằm trong component đang render nữa:
Gõ vài chữ: chỉ SearchBox ghi log.
Truyền qua children
Đôi khi state buộc phải nằm trong một component bao quanh phần tốn kém, như một khung bật tắt tô sáng. Hãy cho component bao đó nhận children. Component cha tạo các phần tử con, và vì component cha không render khi state của component bao thay đổi, những phần tử đó vẫn là cùng các object như trước, nên React bỏ qua chúng.
Nhấn nút bật tắt và chỉ Highlighter ghi log. Giờ hãy chuyển <Article /> vào JSX của Highlighter thay cho {children}, và mỗi lần nhấn sẽ ghi thêm render Article.
Memo hóa
Khi không thể tổ chức lại như hai cách trên, hãy bọc component con trong memo. Khi đó React so sánh props của nó với props trước và bỏ qua lần render khi tất cả đều giống nhau. Object và hàm tạo ra trong lúc render luôn là mới, nên memo thường đi kèm useMemo hoặc useCallback cho những props đó. Trang React.memo trình bày điều này bằng các ví dụ chạy được.
import { memo } from 'react';
const ProductList = memo(function ProductList({ category }) {
// skipped while category stays the same
});
React Compiler có thể tự thêm kiểu memo hóa này cho bạn lúc build, nhưng tổ chức lại state vẫn là cách nên thử đầu tiên: nó loại bỏ công việc thay vì lưu cache kết quả.
Render thường không có vấn đề gì
Một lần render là một lời gọi hàm trả về các object. React chạy hàng nghìn lần như vậy rất nhanh, và một lần render cho ra kết quả giống hệt thì không thay đổi DOM nào. Đừng thêm memo ở khắp nơi "cho chắc": mỗi phép so sánh đều có chi phí riêng và làm code khó đọc hơn. Hãy đo trước. Profiler của React DevTools cho biết component nào đã render, vì sao, và mỗi lần mất bao lâu.
Câu hỏi thường gặp
Điều gì khiến một component React render lại?
Có ba nguyên nhân: state của chính nó thay đổi, component cha render, hoặc một context nó đọc bằng useContext nhận giá trị mới. Props không phải là một nguyên nhân riêng: props mới chỉ đến khi component cha render.
Component con có render lại khi component cha render lại không?
Có. Mặc định, mọi component bên trong một component cha đang render cũng render theo, kể cả khi props của nó không đổi. Bọc component con trong memo giúp React bỏ qua nó khi props giống hệt lần trước.
Virtual DOM trong React là gì?
Đó là cách gọi chung cho cây các object JavaScript thuần (React element) mà component của bạn trả về. React so sánh cây mới với cây trước và chỉ thay đổi những node DOM thật có khác biệt.
Render lại có làm giảm hiệu năng không?
Thường là không. Một lần render là một lời gọi hàm tạo ra các object, và React chỉ động đến DOM ở những chỗ kết quả thay đổi. Chỉ tối ưu khi đo được một lần render thực sự chậm, ví dụ bằng Profiler của React DevTools.
Render và commit khác nhau thế nào?
Render là lúc React gọi các component để biết màn hình cần trông ra sao. Commit là lúc React áp dụng các khác biệt vào DOM. Một lần render cho ra kết quả giống hệt thì không commit thay đổi DOM nào.