+7

Tìm hiểu về Hooks trong React (P2)

1. Mở đầu


Ở bài viết trước mình đã giới thiệu và chia sẻ cho các bạn về tác dụng cũng như cách dùng 3 hooks trong ReactJS lần lượt là useState, useReduceruseEffect. Trong bài viết này mình sẽ tiếp tục giới thiệu thêm về một số hooks khác và cả một số thư viện bên thứ 3 mình hay sử dụng cũng đã hỗ trợ sử dụng hooks. Nếu bạn vẫn chưa đọc bài trước thì có thể xem tại đây

2. React Hook


d. useMemo

Dựa vào tên gọi của hooks nói trên chắc các bạn cũng đoán ra được tác dụng của nó chính là dùng để ghi nhớ (hay có thể gọi là cache lại) một thứ gì đó hay nếu theo trên docs của React thì nó sẽ có tác dụng là Returns a memoized value. useMemo trên thực tế có thể được coi là một trong các phương pháp dùng để tối ưu ứng dụng React của bạn hoặc ngược lại 😄. Nhưng trước tiên chúng ta sẽ đi vào phần tốt trước. useMemo khi chúng ta sử dụng sẽ có dạng như sau:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Về cơ bản useMemo sẽ nhận vào một hàm bất kì của bạn là tham số đầu tiên và tham số thứ 2 là một mảng. Nếu bạn còn nhớ ở bài trước đó thì useEffect cũng nhận vào một mảng như vậy. Đối với cả 2 hooks thì mảng này có vai trò giống hệt nhau, với useEffect thì nếu bất cửa giá trị nào trong mảng này bị thay đổi thì hàm thứ nhất sẽ được gọi là thì điều tương tự cũng xảy ra với useMemo. Với ví dụ trên trong trường hợp biến a hoặc b của chúng ta bị thay đổi thì hàm computeExpensiveValue sẽ được chạy lại và trả về một giá trị mới cho biến memorizedValue. Để rõ hơn thì chúng ta sẽ có một ví dụ ngớ ngẩn và vô nghĩa như sau

const ResultCard = ({x, y}) => {

    function calculateResult() {
        let z = 0;
        while (z < 10000) {
            z++;
        }
        return x + y;
    }

    const result = useMemo(() => {
        let z = 0;
        while (z < 10000) {
            z++;
        }
        return x + y;
    }, [x, y]);

    return (
        <p>Result From Function: {calculateResult()}</p>
        <p>Result From Memorized: {result}</p>
    );
}

Như bạn thấy trên component của chúng ta sẽ có chức năng là nhận vào 2 props là xy và hiển thị ra màn hình của chúng. Ở trong function calculateResultuseMemo ngoài việc trả về kết quả thì mình còn cho thêm một vòng while vô nghĩa để "làm tăng độ phức tạp" 😄 😄 😄. Với cả 2 cách làm trên thì giả sử ở lần thứ nhất ta truyền vào lần lượt là các giá trị là 1, 2 thì cả calculateResultuseMemo sẽ đều đều thực hiện phần nội dung hàm của nó bao gồm chạy một vòng while vô nghĩa và tính kết quả sau đó ta sẽ thu được trên màn hình kết quả là 3. Ở lần tiếp theo ta vẫn tiếp tục truyền vào 2 giá trị là 1, 2 tiếp thì lúc này kết quả mà chúng ta thu được vẫn là 3 😄. Tuy nhiên ở đây đối với hàm calculateResult sẽ tiến hành tính chạy lại toàn bộ nội dung bên trong nó bao gồm chạy vòng while sau đó mới trả về tổng x + y. Còn với useMemo thì khác, như mình đã nói ở trên, nội dung của hàm ta viết trong useMemo sẽ chỉ chạy lại nếu mảng gồm [x. y] bị thay đổi. Do ở lần thứ 2 ta tiếp tục truyền vào 2 giá trị giống lần thứ nhất nên useMemo sẽ trả về ngay giá trị 3 mà không cần chạy lại nội dung hàm. Điều này xảy ra vì với mỗi lần chạy thì useMemo sẽ "ghi nhớ" lại kết quả (giá trị) của lần chạy đó, nếu có giá trị trong dependancy array của bạn bị thay đổi thì useMemo mới chạy lại còn không thì nó sẽ luôn trả về giá trị ghi nhớ trước đó. Trên thực tế ta sẽ dùng hàm này nếu giá trị mà ta dùng cần trải qua một quá trình tính toán phức tạp nào đó (đó là vì sao mình cho cái vòng while vào) thì ta có ta có thể dùng hàm này để lưu lại kết qủa và tái sử dụng lại trong trường hợp dependancy array không có thay đổi. Tuy nhiên bạn không nên lạm dụng hàm này bằng cách sử dụng nó ở mọi nơi vì nó có thể gây ra phản tác dụng là làm cho hiệu năng ứng dụng của bạn tồi tệ hơn.

e. useCallback

Tương tự với useMemo thì ta có hooks tiếp theo là useCallback với tác dụng được biết đến trong docs là Returns a memoized callback. Bạn có thể thấy ở với useMemo sẽ là return value còn useCallbackcallbackuseMemo ta sẽ dùng khi muốn sử dụng ngay giá trị sau khi chay qua phần code phải tính toán phức tạp còn với useCallback sẽ trả về cho chúng ta một function để ta có thể gọi function đó ở các hàm khác hoặc có thể gán vào event handler. Về chi tiết thì hàm useCallback sẽ có nội dung như sau:

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

Sau khi này ta có thể sử dụng hàm memoizedCallback() giống như một function bình thường ở bất cứ đâu ta cần. Mục đích chúng ta có hàm useCallback là vì trong một function component như sau:

const SomeComponent = ({ str }) => {

    function handleThisThing() {

    }

    function handleThatThing() {

    }
    
    function handleOtherThing() {

    }

    return (
        <Fragment>
            <p>Hihi - {str} </p>
            <button onClick={handleThisThing}>THIS</button>
            <button onClick={handleThatThing}>THAT</button>
            <button onClick={handleOtherThing}>OTHER</button>
         </Fragment>
    )
}

Mỗi khi thuộc tính str truyền vào bị thay đổi thì tất nhiên component của chúng ta sẽ bị render lại. Chính vì sự render lại này nên các function chúng ta định nghĩa lần lượt là handleThisThing, handleThatThinghandleOtherThing sẽ được tạo lại cho mỗi lần render. Còn với nếu bạn dùng useCallback thì hàm này sẽ được dùng lại nếu depencancy array không thay đổi thay vì luôn luôn tại mới. Có thể đọc đến đây bạn sẽ thấy useCallback không có nghĩa lắm hoặc chả hiểu gì 😄. Vì thế chúng ta sẽ đi vào một ví dụ cũng vô nghĩa như sau (link):

const Child = ({ handleThis }) => {
  console.log("CHILD RE-RENDER");

  return <h2 onClick={handleThis}>CHILD</h2>;
};

const Parent = () => {
  const [counter, setCounter] = useState(0);

  console.log("PARENT RE-RENDER");

  function handleThis() {
    console.log("THIS");
  }

  function increment() {
    setCounter(counter + 1);
  }

  return (
    <div>
      <h1> Parent Counter: {counter}</h1>
      <button onClick={increment}>INCREMENT</button>
      <Child handleThis={handleThis} />
    </div>
  );
};

Như bạn có thể thấy ở đây chúng ta có 2 component là ParentChild, component Child sẽ được sử dụng trong component Parent và nhận một thuộc tính được truyền vào đó là hàm handlThis. Còn component Parent sẽ có một cái biến counter và một button + 1 function dùng để tăng cái biến counter đó lên một đơn vị. Đồng thời ở đây mình cũng đã setup cho phép bạn có thể nhìn thấy mỗi lần component của chúng ta re-render sẽ console.log một đoạn text với cả 2 component. Bạn có thể dễ dàng nhận ra là mỗi khi ta bấm button INCREMENT thì biến counter của chúng ta sẽ tăng lên và kéo theo component Parent của chúng ta được render lại và kéo theo cả component Child được render lại. Đối với component Child điều này là không hợp lý vì bản thân component Child vẫn hiển thị ra nội dung y hệt không có gì bị thay đổi cả. Chính vì thế chúng ta sẽ tiến hành tối ưu việc re-render của component Child bằng cách sử dụng hàm React.memo nếu bạn chưa biết hàm này thì có thể đọc tại đây. Về cơ bản thì React.memo sẽ có khả năng ghi nhớ lại component của chúng ta và chỉ thực hiện re-render lại khi cần thiết (khi có state thay đổi hoặc props thay đổi). Ta sẽ viết lại như sau:

const Child = React.memo(({ handleThis }) => {
  console.log("CHILD RE-RENDER");

  return <h2 onClick={handleThis}>CHILD</h2>;
});

const Parent = () => {
  const [counter, setCounter] = useState(0);

  console.log("PARENT RE-RENDER");

  function handleThis() {
    console.log("THIS");
  }

  function increment() {
    setCounter(counter + 1);
  }

  return (
    <div>
      <h1> Parent Counter: {counter}</h1>
      <button onClick={increment}>INCREMENT</button>
      <Child handleThis={handleThis} />
    </div>
  );
};

Trong trường hợp này của chúng ta React.memo sẽ chỉ tiến hành re-render lại component khi mà props truyền vào là handleThis bị thay đổi. Bằng việc sử dụn React.memo ta mong muống component Child của chúng ta sẽ không bao bị re-render lại mỗi khi ta bấm nút INCREMENT cả. Tuy nhiên trên thực tế nếu bạn tiếp tục bấm nút INCREMENT ở component Parent sẽ thấy cả hai vẫn bị re-render lại như trước kia và hàm React.memo không hoạt động như ta mong đợi. Điều này xảy ra là do thực tế mỗi lần component Parent của chúng ta re-render thì nó sẽ tạo ra một function handleThis mới và truyền vào cho component Child, vì vậy React.memo sẽ luôn coi đây là thay đổi props và sẽ render lại component Child. Bạn có thể kiểm chứng tại đây. Đây chính là lúc mà useCallback phát huy tác dụng, ta sẽ đưa useCallback vào cuộc chơi bằng cách sửa lại code như sau:

const Child = React.memo(({ handleThis }) => {
  console.log("CHILD RE-RENDER");

  return <h2 onClick={handleThis}>CHILD</h2>;
});

const Parent = () => {
  const [counter, setCounter] = useState(0);

  console.log("PARENT RE-RENDER");

  const handleThis = useCallback(() => {
      console.log("THIS");
 }, []);

  function increment() {
    setCounter(counter + 1);
  }

  return (
    <div>
      <h1> Parent Counter: {counter}</h1>
      <button onClick={increment}>INCREMENT</button>
      <Child handleThis={handleThis} />
    </div>
  );
};

Với việc sử dụng useCallback thì funciton handleThis của chúng ta sẽ không bị tạo lại nữa mà sẽ được "ghi nhớ" lại để mỗi lần re-render lại component Parent sẽ đem ra sử dùng luôn hay đem ra truyền cho component Child thay vì sử dụng lại. Chính vì thế lúc này component Child của bạn sẽ đồng thời không còn bị re-render lại nữa. Tất nhiên bạn cũng có thể kiểm nghiệm lại tại đây. Tuy nhiên mình lại muốn nhắc lại với các bạn một lần nữa là không nên dùng useCallback ở mọi nơi mà hãy thực hiện các biện pháp đo lường để biết được ứng dụng của bạn đang gặp vấn đề hiệu năng ở chỗ nào trước khi quyết định sử dụng useCallback hay bất cứ function nào khác. Vì bất cữ biện pháp tối ưu hóa nào bạn đưa ra hay sử dụng sẽ đi kèm một tác dụng phụ nhất định nên bạn cần cân đo đong đếm về tác dụng và tác hại của mỗi biện pháp.

3. Kết bài


Bài viết của mình đến đây là kết thúc, nếu các bạn có phần nào đọc không hiểu có thể comment ngay ở phía dưới mình sẽ hỗ trợ việc giải đáp hoặc nếu có phần nào mình viết không đúng thì cũng hay chia sẽ xuống phía dưới để mình có thể kịp thời cập nhật lại. Cám ơn các bạn đã đọc bài viết và đừng quên đề lại 1 upvote nhé 😄.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí