useMemo lưu cache kết quả của một phép tính giữa các lần render. React chạy hàm của bạn ở lần render đầu tiên, giữ lại kết quả, và trả về đúng kết quả đó ở các lần render sau cho đến khi một trong các giá trị trong mảng dependency thay đổi.
Mở Console và đổi theme vài lần: không có gì được ghi log, và nút chuyển phản hồi tức thì. Đổi tab thì filtering for active xuất hiện, kèm một khoảng dừng ngắn, vì tab là một dependency. Giờ thay dòng useMemo bằng const visible = filterTodos(todos, tab); rồi đổi theme lần nữa: lần click nào cũng ghi log và khựng lại 100 ms.
Cú pháp
const cachedValue = useMemo(calculateValue, dependencies);
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
calculateValuelà một hàm không có đối số, trả về giá trị bạn muốn cache. React gọi nó trong lúc render.dependencieslà danh sách mọi giá trị trong component mà hàm đó đọc tới: props, state và các biến khai báo trong thân component.- Ở mỗi lần render, React so sánh từng dependency với giá trị của nó ở lần render trước bằng
Object.is. Nếu tất cả đều khớp, nó trả về giá trị đã cache mà không gọi hàm của bạn. Nếu có cái nào khác, nó gọi hàm và cache kết quả mới.
useMemo là một hook, nên nó tuân theo quy tắc của hook: gọi nó ở cấp cao nhất của component, không đặt trong vòng lặp hay câu lệnh điều kiện.
Dependency phải đầy đủ
Bỏ sót một giá trị khỏi mảng thì cache sẽ tiếp tục trả về kết quả tính từ giá trị cũ của nó. Ở đây tổng tiền đọc qty nhưng chỉ liệt kê price:
Click Quantity: nó lên 2, nhưng tổng vẫn là 10 và không có gì được ghi log. Click Price thì tổng nhảy lên 22, vì chỉ lúc đó phép tính mới chạy lại và đọc qty hiện tại. Thêm qty vào mảng thì cả hai nút đều cập nhật tổng. Trong dự án thật, quy tắc lint react-hooks/exhaustive-deps sẽ bắt được lỗi này.
Lỗi ngược lại là một dependency mới tinh ở mỗi lần render, chẳng hạn một object tạo trong thân component. Nó không bao giờ khớp với giá trị trước, nên cache không bao giờ được dùng:
const filters = { tab, query }; // a new object every render
const visible = useMemo(() => applyFilters(todos, filters), [todos, filters]); // recomputes every time
// List the primitive values instead
const visible = useMemo(() => applyFilters(todos, { tab, query }), [todos, tab, query]);
Giữ object ổn định cho component con dùng memo
Lý do thứ hai để dùng useMemo chẳng liên quan gì tới phép tính chậm. Một component con được bọc trong memo chỉ bỏ qua render khi mọi prop vẫn giống lần trước. Một object literal viết trong thân component là object mới ở mỗi lần render, nên nó vô hiệu hóa memo kể cả khi nội dung không đổi. useMemo giữ nguyên object đó cho đến khi đầu vào của nó thay đổi.
Gõ vào ô ghi chú thì Chart vẫn im lặng, còn Switch unit ghi một lần render. Thay lời gọi useMemo bằng const options = { unit, max: unit === 'km' ? 100 : 62 }; thông thường thì mỗi lần gõ phím trong ô ghi chú đều ghi render Chart.
Điều tương tự áp dụng cho một effect phụ thuộc vào object. Nếu effect liệt kê options trong dependency và options được tạo lại ở mỗi lần render, effect sẽ chạy sau mỗi lần render. useMemo sửa được chuyện đó, nhưng cách đơn giản hơn thường là tạo object bên trong effect và phụ thuộc vào các giá trị nguyên thủy:
useEffect(() => {
const options = { serverUrl, roomId };
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
Khi nào không nên dùng useMemo
Hầu hết phép tính trong một component đều nhẹ: cộng vài số, nối hai chuỗi, lọc vài chục phần tử. Cache những thứ đó gần như chẳng tiết kiệm được gì: React vẫn phải lưu giá trị và so sánh dependency ở mỗi lần render, còn code thì khó đọc hơn.
// No useMemo needed
const fullName = first + ' ' + last;
const doneCount = todos.filter((t) => t.done).length;
Để biết thứ gì đó có chậm không, hãy đo thời gian. Bọc phép tính trong console.time('filter') và console.timeEnd('filter'), thực hiện thao tác, rồi xem con số. Một cách ước lượng: việc tốn từ một mili giây trở lên trên máy bạn là ứng viên, nhớ rằng thiết bị của người dùng có thể chậm hơn. Trước khi dùng useMemo, hãy xem việc đó có nên nằm trong render hay không: dữ liệu không bao giờ đổi có thể tính một lần bên ngoài component, và state chỉ một phần nhỏ của trang dùng tới có thể chuyển vào một component nhỏ hơn (xem render).
useMemo và useEffect cho giá trị dẫn xuất
Một cách làm phổ biến là tính giá trị trong effect rồi lưu vào state:
// Two renders per change, and the first one has stale data
const [visible, setVisible] = useState([]);
useEffect(() => {
setVisible(filterTodos(todos, tab));
}, [todos, tab]);
// One render, always in sync
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
Phiên bản dùng effect render một lần với visible cũ, rồi chạy effect, set state và render lại. Đó là một lần render thừa, và ở lần đầu danh sách không khớp với tab. Tính trong lúc render bằng useMemo (hoặc không cần hook nào, nếu việc đó nhẹ) cho ra giá trị đúng ngay trong cùng lần render. Effect dùng để đồng bộ với những thứ nằm ngoài React, không phải để suy ra một state từ state khác.
Gợi ý về hiệu năng, không phải cam kết
useMemo cho React biết nó có thể dùng lại một kết quả. Nó không hứa rằng React sẽ làm vậy. Khi hoạt động bình thường React giữ giá trị đã cache, nhưng có thể bỏ nó đi, chẳng hạn khi bạn sửa tệp trong môi trường phát triển, hoặc khi một component suspend trong lần mount đầu tiên. Hãy viết code sao cho vẫn đúng nếu phép tính chạy lại: hàm phải thuần, không có side effect như gửi request, subscription hay thay đổi biến bên ngoài nó. Side effect thuộc về event handler hoặc effect.
Trong môi trường phát triển, dưới StrictMode, React gọi hàm tính toán của bạn hai lần để giúp bạn phát hiện code không thuần. Bản xem trước ở đây chạy như một bản build production, nên bạn sẽ thấy mỗi log filtering for một lần.
useMemo và React Compiler
React Compiler 1.0 là một công cụ build phân tích các component và tự động thêm memoization, cache phép tính và JSX ở những chỗ mà useMemo, useCallback và memo có ích. Trong dự án có dùng nó, bạn sẽ ít phải viết tay những thứ này. useMemo viết tay vẫn hoạt động song song với compiler, vẫn là công cụ cần dùng khi chưa thiết lập compiler, và bạn sẽ gặp nó trong phần lớn code React hiện có.
Câu hỏi thường gặp
useMemo trong React dùng để làm gì?
Nó gọi hàm tính toán của bạn ở lần render đầu tiên, lưu kết quả lại, và ở các lần render sau trả về kết quả đã lưu miễn là mọi giá trị trong mảng dependency vẫn giống lần trước (so sánh bằng Object.is).
Khi nào nên dùng useMemo?
Khi một phép tính chậm thấy rõ và đầu vào của nó ít khi đổi, hoặc khi bạn cần một object hay mảng giữ nguyên tham chiếu giữa các lần render vì một component con dùng memo hoặc một effect phụ thuộc vào nó. Với việc nhẹ nhàng, hãy bỏ qua nó.
useMemo và useCallback khác nhau thế nào?
useMemo lưu cache giá trị mà hàm của bạn trả về. useCallback lưu cache chính hàm đó. useCallback(fn, deps) tương đương với useMemo(() => fn, deps).
useMemo có đảm bảo giá trị không bao giờ bị tính lại không?
Không. Nó chỉ là một gợi ý về hiệu năng. Trong một số trường hợp React có thể bỏ giá trị đã cache, ví dụ khi bạn sửa tệp trong môi trường phát triển, nên code của bạn vẫn phải chạy đúng nếu phép tính chạy lại.
Có React Compiler thì còn cần useMemo không?
React Compiler có thể tự động thêm memoization khi bản build của bạn dùng nó. Khi không có compiler, hoặc ở những chỗ nó bỏ qua, useMemo viết tay vẫn hoạt động như cũ và vẫn rất phổ biến trong code hiện có.