Bạn có thực sự hiểu useMemo và useCallback trong React?

Khi các bạn làm việc với React thì việc sử dụng các hooks là rất thường xuyên như useState, useRef, hay useEffect. Đi kèm với các hooks phổ biến này thì cũng có những hooks khác mà lâu lâu các bạn thấy nhiều người dùng nó như useMemouseCallback.

Khi được hỏi thì đa số người ta sẽ trả lời dùng nó để tối ưu về vấn đề re-render hoặc là các tính toán phức tạp(expensive calculation)để cải thiện hiệu suất của ứng dụng. Rồi dùng nó vô tội vạ ở mọi nơi để ứng dụng chạy cho “nhanh” theo cách họ nghĩ. Nhưng bản chất React đã rất là nhanh rồi. Việc lạm dụng useMemouseCallback đôi khi không phải là điều tốt mà còn làm cho ứng dụng ở trên chậm hơn.

Ở bài viết này mình sẽ giúp các bạn hiểu rõ hơn về chúng để từ đó biết khi nào nên sử dụng và khi nào không nên để giúp cho code của các bạn clean hơn và ứng dụng của các bạn hoạt động tốt hơn. Vậy chức năng chính của useMemo và useCallback là gì ?

Hiểu đơn giản thì nó sẽ ghi nhớ(Memoized) giá trị giữa các lần mà Component re-render.

Nếu một giá trị hoặc hàm được bọc lại trong một trong các hook đó, thì React sẽ lưu nó vào bộ nhớ đệm trong lần hiển thị đầu tiên(initial render) và trả về tham chiếu(reference) đến giá trị đã lưu đó trong các lần hiển thị liên tiếp(re-render).

Nếu không có nó, các giá trị như array, object hoặc function sẽ được tạo lại từ đầu khi mà Component re-render. Dưới đây là một ví dụ cho các bạn dễ hình dung trong Javascript hay gặp khi so sánh object với nhau(tương tự cho array và function)

const a = {name: "evondev"};
const b = {name: "evondev"};
console.log(a === b); // false
const c = a;
console.log(a === c); // true

Các bạn thấy khoản so sánh a === b sẽ luôn là false, nhưng khi các bạn khai báo c = a nghĩa là c đang tham chiếu tới a cho nên a === c sẽ là true. Các bạn có thể tìm hiểu về by referrences trong Javascript tại đây: https://javascript.info/object-copy

Từ điều trên thì cách hoạt động của useMemo và useCallback cũng tương tự(c===a) như này:

function App() {
  const value = {name: 'evondev'};
  useEffect(() => {
    // do something
  }, [value]);
  return (
    <div>App</div>
  )
}

Mỗi lần App re-render thì useEffect sẽ luôn chạy bởi vì value đang là object. Ban đầu là {name: 'evondev'}, lần re-render tiếp theo nó vẫn là {name: 'evondev'} nhưng khi so sánh(a===b) nó sẽ không bằng nhau cho nên dependencies trong useEffect thay đổi thì nó chắc chắn sẽ chạy vào lại useEffect.

Vậy làm sao để giải quyết vấn đề ở trên để sau mỗi lần re-render thì giá trị của biến value được giữ nguyên chứ không thay đổi ? Lúc này useMemo đến để giải quyết nó như sau

const value = useMemo(() => {name: 'evondev'}, []);
useEffect(() => {
    // do something
}, [value]);

Đoạn code sử dụng useMemo nó tương tự như đoạn dưới đây, c đang reference tới a cho nên là khi so sánh của lần re-render trước đó với lần tiếp theo sẽ là true và dependencies ở useEffect phía trên kia sẽ không bị chạy lại

const a = {name: "evondev"};
const b = {name: "evondev"};
const c = a;
console.log(a === c); // true

Tips: Khi các bạn làm việc với VSCode, có thiết lập eslint trong quá trình code nếu các bạn sử dụng object, array hay function làm dependencies cho useEffect thì VSCode nó sẽ thông báo ra như thế này giúp các bạn luôn

The ‘value’ object makes the dependencies of useEffect Hook (at line 74) change on every render. Move it inside the useEffect callback. Alternatively, wrap the initialization of ‘value’ in its own useMemo() Hook.eslint(react-hooks/exhaustive-deps)

Tương tự khi các bạn làm việc với useCallback nó trông như thế này:

const Component = () => {
  const fetch = useCallback(() => {
  }, []);

  useEffect(() => {
    fetch();
  }, [fetch]);
  return null;
};

Lưu ý: Các bạn nên nhớ một điều là useMemouseCallback rất là hữu ích khi dùng cho việc lưu nhớ giá trị giữa các lần re-render ngoại trừ initial render(khởi tạo lần đầu nó làm cho React phải xử lý thêm nhiều việc vì React sẽ lưu nó vào bộ nhớ đệm trong lần hiển thị đầu tiên) cho nên nếu các bạn sử dụng ít thì không sao nhưng sử dụng nó ở hàng trăm hàng ngàn chỗ thì có thể xảy ra vấn đề đó.

Dưới đây là một đoạn code ví dụ khác về việc sử dụng useMemo và useCallback vô tội vạ:

const Item = ({ item, onClick }) => <button onClick={onClick}>{item.name}</button>;

const Component = ({ data }) => {
  const value = useMemo(() => ({ a: someStateValue }), [someStateValue]);
  const onClick = useCallback(() => {
    console.log(value);
  }, [value]);

  return (
    <>
      {data.map((d) => (
        <Item item={d} onClick={onClick} />
      ))}
    </>
  );
};

Có thể các bạn đang viết như vậy hoặc thấy ai đó viết như thế này rồi tự hào là ngon lành cành đào. Mọi thứ đã được tối ưu và giúp cho ứng dụng chạy ngon các thứ nhưng thực chất không phải như vậy. Đoạn code ở trên sử dụng useMemouseCallback nhưng không có tác dụng gì cả, nói thẳng ra là vô dụng, không cần thiết, còn làm chậm lần render đầu tiên nữa(initial render).

Để hiểu lý do tại sao, chúng ta cần nắm được một điều quan trọng trong React đó chính là tại sao component trong React có thể re-render.

Đây chắc chắn có thể là một câu hỏi khi đi phỏng vấn về React, câu trả lời sẽ thường là khi state hoặc props thay đổi thì component sẽ bị re-render. Ủa vậy thì nếu props hoặc state không thay đổi hoặc đã được ghi nhớ với useMemo và useCallback thì sẽ hoạt động tốt chứ ?

Nhưng có một điều quan trọng đó chính là nếu component cha của chúng(parent component) re-render thì sao ? Hoặc khi chính component đó re-render thì toàn bộ các phần tử con của chúng cũng sẽ re-render. Hãy xem ví dụ dưới đây:

const App = () => {
  const [state, setState] = useState(1);

  return (
    <div className="App">
      <button onClick={() => setState(state + 1)}> click to re-render {state}</button>
      <br />
      <Page />
    </div>
  );
};

Điều gì sẽ xảy ra khi chúng ta nhấn vào button, state sẽ thay đổi và sẽ làm cho App re-render. Component <Page/> không nhận vào bất kỳ props và cũng không có state nào của riêng nó, nhưng nó vẫn bị re-render, và thậm chí con của nó là <Item/> cũng bị render theo nốt.

const Page = () => <Item className="bg-blue-500"></div>

Vậy làm sao để cải thiện vấn đề này đây ? Đó chính là sử dụng useMemo hoặc có thể sử dụng React.memo để ghi nhớ nó như sau

const App = () => {
  const [state, setState] = useState(1);
  const pageMemo = useMemo(() => <Page />, []);
  return (
    <div className="App">
      <button onClick={() => setState(state + 1)}> click to re-render {state}</button>
      <br />
      {pageMemo}
    </div>
  );
};

Tuyệt vời 🤩 Component Page không còn bị re-render không cần thiết nữa rồi. Nhưng đâu đơn giản vậy, lỡ như nó có thêm các props khác(onClick) nữa thì sao ? Mặc dù Page đã có dùng memo bọc lại rồi liệu nó có chạy không ? Các bạn đoán xem sao.

const App = () => {
  const [state, setState] = useState(1);
  const onClick = () => {
    console.log('Do something on click');
  };
  return (
    // other code ago
    <Page onClick={onClick} />
  );
};
function Page({ onClick }) {
  console.log("re-render page");
  return <div></div>;
}
const PageMemo = memo(Page);

Nếu các bạn đoán nó vẫn chạy vào dòng console.log("re-render page") thì đúng rồi đấy bởi vì onClick đang là function, là props của Page. Nó luôn thay đổi sau mỗi lần chúng ta nhấn vào button. Để xử lý triệt để thì chúng ta sẽ bọc onClick vào useCallback như thế này

const onClick = useCallback(() => {
    console.log("click page");
  }, []);

Lại quá tuyệt vời rồi 🔥 Khi App re-render nó chạy tới thằng Page nhưng thằng Page lại được ghi nhớ(memoized) nên nó bỏ qua khoản re-render thằng Page này, tiếp đến nó sẽ check props của thằng Page xem có không. Ở đây có props onClick là một function, nó sẽ kiểm tra tiếp xem onClick đã được ghi nhớ hay chưa. Nếu chưa thì sẽ re-render lại, nếu có ghi nhớ với useCallback rồi thì thôi.

Còn nữa giả như mình thêm vào Page một props có tên là value={[1,2,3]} thì sao nhỉ ? 🤔 Úi lại toang nữa rồi, value có giá trị là array cho nên nó luôn thay đổi sau mỗi re-render. Tất nhiên là Page lại bị re-render tiếp 😢

Để tối ưu chúng tại lại phải bọc value vào useMemo và thế là vấn đề sẽ được giải quyết

const value = useMemo(() => [1, 2, 3], []);

Từ tất cả những điều trên đúc kết lại cho chúng ta một điều khi ghi nhớ (memoized) với useMemo và useCallback cho các props trên Component đó chính là: Khi tất cả các props và chính Component đó đều được ghi nhớ(memoized). Còn không thì tất cả mọi thứ đều vô nghĩa.

Các bạn có thể hoàn toàn bỏ useMemouseCallback khi:

  • Các bạn truyền props trực tiếp hoặc vào dependencies(useEffect) cho một Component mà Component đó không được ghi nhớ(Memoized)
  • Các bạn truyền props trực tiếp hoặc vào dependencies(useEffect) cho một Component mà Component đó có ít nhất 1 props không được ghi nhớ(memoized)
  • Lưu ý: Props là các giá trị như object, array hay function

Một câu hỏi được đặt ra là Tại sao không tối ưu mà lại đi xóa bỏ chúng? Đơn giản là khi các bạn làm việc nếu các bạn thấy Component của các bạn có vấn đề về hiệu suất thì chắc chắn các bạn sẽ giải quyết nó, đúng chứ ? Nhưng nếu Component của bạn không có 1 bất kỳ vấn đề nào về hiệu suất cả. Vậy thêm useMemo và useCallback để làm gì ?

Ở đầu bài mình có nhắc tới từ khóa là re-render và expensive calculation. Vậy expensive calculation là gì ? Và dựa vào tiêu chí nào để xác định cho giá trị là expensive calculation ? Mình sẽ chỉ cho các bạn ở phần update sau nhé.

Subscribe
Notify of
guest

5 Comments
Inline Feedbacks
View all comments
Bảy
Bảy

Hi ad, bài viết khá hay nhưng mình vẫn còn một thắc mắc giữa useMemo và useCallback mong ad giải đáp. ·

Cả 2 đều dùng để tối ưu performance, useCallback dùng để memorise function. useMemo để tối ưu các expensive cal. Tuy nhiên mình vẫn có thể dùng useMemo để ghi nhớ một hàm….

const {cb1,cb2 } = useMemo(() => {

const cb1 = () => {};

const cb2 = () => {};

return {cb1, cb2}

}, [])

Mong ad giải đáp·

Harribel
Harribel
Reply to  Bảy

– Ở trang chủ của react đã có ví dụ về việc dùng useMemo để ghi nhớ một hàm rồi mình sẽ để link tại đây để bạn xem nhé https://react.dev/reference/react/useMemo#memoizing-a-function. – Còn như ví dụ trên của bạn thì theo mình việc destructuring để lấy ra từng hàm cb1, cb2 khiến cho việc dùng useMemo để ghi nhớ object chứa 2 hàm cb1, cb2 trở nên vô nghĩa do mỗi lần component re-render sẽ lại tại ra 2 biến mới là cb1 và cb2 để lưu trữ địa chỉ của 2 function đó (do destructuring chỉ là cách viết… Read more »

Elsie
Elsie
Reply to  Bảy

thật ra useMemo là ghi nhớ lại (đối với các kiểu dữ liệu primitive value thì ghi nhớ giá trị, còn đối với các dạng như object, array, func thì lại ghi nhớ dưới dạng địa chỉ), nên bạn có thể dùng useMemo để ghi nhớ một hàm thay cho useCallBack.

Happy coding!