memo cho phép một component bỏ qua việc render lại khi props giống hệt lần trước. Bọc component trong memo(...) và mỗi khi component cha render, React sẽ so sánh từng prop mới với prop cũ trước; nếu tất cả đều bằng nhau, React dùng lại kết quả trước đó thay vì gọi lại component.
Gõ vào ô nhập: giao diện vẫn theo kịp, và Console chỉ hiện render SlowList một lần, từ lần render đầu tiên. Bỏ phần bọc memo( (cùng dấu đóng ngoặc của nó) rồi gõ lại: giờ mỗi lần gõ phím đều phải chờ danh sách 150 ms và ghi thêm một dòng render.
Cú pháp
import { memo } from 'react';
const MemoizedComponent = memo(SomeComponent, arePropsEqual?);
SomeComponentlà bất kỳ function component nào. Cách viết thường gặp là bọc ngay tại chỗ định nghĩa:const Card = memo(function Card(props) { ... }). Đặt tên cho hàm bên trong giúp giữ tên đó trong React DevTools và trong thông báo lỗi.arePropsEquallà tùy chọn và hiếm khi cần đến (xem bên dưới).- Kết quả là một component bạn render như mọi component khác.
Mặc định React so sánh props cũ và mới theo kiểu nông (shallow): nó kiểm tra từng prop bằng Object.is. Chuỗi, số và boolean được so sánh theo giá trị, nên title="Hi" bằng title="Hi". Object, mảng và hàm được so sánh theo tham chiếu: hai object có nội dung giống nhau vẫn là hai object khác nhau.
Trong ví dụ trên, items được khai báo bên ngoài component, nên đó là cùng một mảng ở mọi lần render và phép so sánh luôn đúng.
Object và hàm mới làm memo mất tác dụng
Mọi thứ viết dưới dạng literal trong thân component cha đều được tạo lại ở mỗi lần render: { name: 'Ada' }, [1, 2, 3], () => save(). Truyền một giá trị như vậy cho component memo thì prop đó không bao giờ bằng giá trị trước.
Mỗi lần click đều ghi render Badge object prop nhưng không bao giờ ghi bản dùng chuỗi. Có ba cách sửa trường hợp object:
- Truyền giá trị nguyên thủy. Dùng
name="Ada"thay vìuser={{ name: 'Ada' }}khi component con chỉ cần một hai trường. - Đưa hằng số ra ngoài component. Một object không bao giờ thay đổi có thể nằm ở cấp module, như
itemstrong ví dụ đầu tiên. - Lưu cache. Bọc object và mảng được tạo từ props hoặc state trong useMemo, và bọc hàm trong useCallback.
children cũng gặp vấn đề tương tự. <Card><p>Hello</p></Card> tạo một object phần tử <p> mới ở mỗi lần render, nên một Card dùng memo mà nhận children sẽ render lại mọi lúc.
Những gì memo không chặn được
memo chỉ xem xét props đến từ component cha. Một component đã memo hóa vẫn render khi:
- state của chính nó thay đổi, qua
useStatehoặcuseReducercủa nó; - một context nó đọc thay đổi, qua
useContext.
Nhấn Unrelated thì Panel được bỏ qua. Nhấn Switch theme thì nó render, vì nó đọc context. Nhấn Like thì nó render, vì state của chính nó đã thay đổi. Đó là hành vi đúng như thiết kế: memo dùng để bỏ qua công việc khi không có gì mà component phụ thuộc vào thay đổi, và context lẫn state đều là những thứ nó phụ thuộc vào.
Hàm so sánh tùy chỉnh
Đối số thứ hai thay thế phép so sánh nông. Hàm này nhận props trước và props tiếp theo, rồi trả về true khi chúng được xem là bằng nhau, nghĩa là React bỏ qua lần render.
const MemoChart = memo(Chart, (prev, next) => {
return (
prev.points.length === next.points.length &&
prev.points.every((p, i) => p === next.points[i]) &&
prev.color === next.color &&
prev.onSelect === next.onSelect
);
});
Bạn hiếm khi cần đến hàm này, và rất dễ viết sai. Nếu hàm bỏ sót một prop, component sẽ tiếp tục hiển thị giá trị cũ của prop đó. Bỏ sót một prop là hàm là trường hợp tệ nhất: component bị bỏ qua sẽ tiếp tục gọi hàm cũ, và hàm đó đọc state cũ. Hãy so sánh mọi prop, và đừng so sánh sâu trên dữ liệu lớn, vì khi đó phép so sánh còn tốn hơn lần render nó tiết kiệm được. Giữ props ổn định bằng useMemo và useCallback gần như luôn là cách sửa tốt hơn.
Kiểm tra memo có hoạt động không
Cách kiểm tra nhanh nhất là cách dùng trên trang này: một console.log ở đầu component. Nếu nó in ra khi bạn mong component được bỏ qua, hãy log cả props và so sánh với lần trước để tìm prop đã thay đổi. Trong dự án thật, React DevTools giúp được nhiều hơn: bật "Highlight updates when components render" trong phần cài đặt để thấy component nào nhấp nháy sau mỗi thao tác, còn tab Profiler ghi lại mọi lần render và, khi bật tùy chọn tương ứng, cho biết vì sao từng component render.
Có hai trường hợp trông giống memo không hoạt động nhưng thực ra không phải:
- Lần render đầu tiên luôn xảy ra.
memochỉ bỏ qua các lần render sau khi component đã render một lần với những props đó. - Bản build development có thể render nhiều hơn. Với StrictMode, React render mỗi component hai lần trong môi trường development để phát hiện code không thuần (impure). Bản xem trước ở đây chạy như bản build production, nên mỗi dòng log ở trên tương ứng với một lần render thật.
Khi nào nên dùng memo
memo đáng dùng khi một component render chậm và component cha của nó thường render lại với cùng props cho nó: một danh sách dài hoặc một biểu đồ cạnh ô nhập, một thanh bên trong trang cập nhật mỗi giây. Với component render nhanh, hoặc component có props thay đổi ở mọi lần render, memo chỉ thêm một phép so sánh mà không đem lại gì. Trước khi memo hóa, hãy xem bạn có thể tổ chức lại code không, bằng cách đưa state xuống thấp hơn hoặc truyền JSX qua children, như trong trang render.
React Compiler 1.0, nếu dự án của bạn dùng nó, sẽ tự động memo hóa component và props của chúng lúc build, khiến phần lớn các lời gọi memo thủ công trở nên không cần thiết. Nếu không có nó, memo hoạt động đúng như mô tả ở đây và vẫn rất phổ biến trong code React.
Câu hỏi thường gặp
React.memo dùng để làm gì?
Nó trả về một phiên bản đã memo hóa của component. Khi component cha render, React so sánh props mới với props trước đó và bỏ qua lần render của component nếu mọi prop đều bằng nhau theo Object.is.
Vì sao component dùng memo vẫn render lại?
Thường là do một prop là object, mảng hoặc hàm mới được tạo trong lúc component cha render, nên nó không bao giờ bằng giá trị trước. Component cũng render khi state của chính nó thay đổi hoặc khi một context nó đọc thay đổi, dù có memo hay không.
React.memo khác useMemo thế nào?
memo bọc một component và bỏ qua lần render của nó. useMemo là một hook bên trong component, lưu lại một giá trị đã tính. Hai thứ thường đi cùng nhau: useMemo giữ cho prop là object ổn định để memo có thể bỏ qua render.
Có nên bọc mọi component trong memo không?
Không. Phần lớn lần render đều rẻ, và việc so sánh props cũng có chi phí riêng. Hãy dùng memo cho component render chậm và thường nhận lại đúng những props cũ.
memo có hoạt động với children không?
Hiếm khi. JSX truyền vào children là một object mới ở mỗi lần render, nên component memo nhận children gần như luôn render lại.