Tìm hiểu về React.memo(), useMemo() và useCallback()

1. React memo

  • Là một Higher Order Component (HOC). Higher-Order Components là 1 kỹ thuật nâng cao của React để sử dụng lại logic của component. Nói ngắn gọn một HOC là 1 hàm nhận vào 1 component và trả về 1 component mới.
  • Giúp ghi nhớ lại các props của 1 component để quyết định có render lại component đó hay không -> tối ưu về hiệu năng (Tránh render lại component trong các tình huống không cần thiết)

Ví dụ:
Mình sẽ tạo 1 component con là Content.js và import vào App.js

App.js

import { useState } from "react"
import Content from './Content'
 
function App() {
 
  const [number, setNumber] = useState(0)
 
  const handleIncrease = () => setNumber(number => number + 1)
 
  return (
    <div>
      <h1>{number}</h1>
      <button onClick={handleIncrease}>Click me!</button>
      <Content />
    </div>
  )
}
 
export default App

Mình sẽ tạo 1 state để lưu biến number và 1 hàm handleIncrease để set lại state (tăng thêm 1 đơn vị) mỗi lần ấn vào button click me!.

Content.js

function Content() {
  console.log("Content rerender...")
  return (
    <h1>Haposoft</h1>
  )
}
 
export default Content

Trong Content.js mình có console.log("Content rerender...") để kiểm tra mỗi lần Component bị re-render.

Vấn đề xảy ra khi chúng ta bắt đầu click vào button Click me!.

Ta nhận thấy ngoài lần đầu Content được mounted vào DOM thì mỗi lần click component Content đều bị re-render.
Bởi vì Content là component con của App nên mỗi khi App được re-render thì Content cũng bị re-render.
Đây là điều không cần thiết vì các props của component Content không hề bị thay đổi.

Áp dụng React memo cho component Content

import { memo } from "react"
 
function Content() {
  console.log("Content rerender...")
  return (
    <h1>Haposoft</h1>
  )
}
 
export default memo(Content)

Kết quả:

Ta nhận thấy ngoài lần đầu Content được mounted vào DOM thì mỗi lần click component Content không hề bị re-render nữa.

2. Hook useCallback

Khái niệm: Là một React Hooks giúp mình tạo ra một memoized callback và chỉ tạo ra callback mới khi dependencies thay đổi.

  • Nhận vào 2 tham số: 1 là function, 2 là dependencies và trả về 1 memoized callback.
  • Chỉ tạo ra function mới khi dependencies thay đổi.
  • Nếu dùng empty dependencies thì không bao giờ tạo ra function mới.

Ví dụ:
Thay đổi 1 chút về ví dụ trên, mình sẽ chuyển button Click me! vào trong component Content

App.js

import { useState } from "react"
import Content from './Content'
 
function App() {
 
  const [number, setNumber] = useState(0)
 
  const handleIncrease = () => setNumber(number => number + 1)
 
  return (
    <div>
      <h1>{number}</h1>
      <Content onIncrease={handleIncrease} />
    </div>
  )
}
 
export default App

Để component Content có thể nhận được hàm handleIncrease bên App.js mình sẽ truyền cho nó 1 prop là onIncrease chính là hàm handleIncrease.

Content.js

import { memo } from "react"
 
function Content({ onIncrease }) {
  console.log("Content rerender...")
  return (
    <>
      <h1>Haposoft</h1>
      <button onClick={onIncrease}>Click me!</button>
    </>
  )
}
 
export default memo(Content)

Và bên Content mình dùng destructuring của ES6 để nhận lại onIncrease.

Kết quả:

Ta nhận thấy sau mỗi lần click vào button thì Content bị re-render. Tại sao lại vậy, tại sao có React memo rồi mà vẫn bị re-render?
Giải thích: Đây là kiến thức về reference type của JS, mỗi lần App.js được re-render thì nó tạo ra các hàm handleIncrease khác nhau. Vì vậy, khi truyền handleIncrease làm prop của component Content thì React memo sẽ hiểu là các hàm handleIncrease khác nhau và vẫn quyết định re-render. Đây là điều không cần thiết.

Ứng dụng useCallback
App.js

import { useCallback, useState } from "react"
import Content from './Content'
 
function App() {
 
  const [number, setNumber] = useState(0)
 
  const handleIncrease = useCallback(() => {
    setNumber(number => number + 1)
  }, [])
 
  return (
    <div>
      <h1>{number}</h1>
      <Content onIncrease={handleIncrease} />
    </div>
  )
}
 
export default App;

Ta sẽ ứng dụng hook useCallback cho hàm handleIncrease. Vì đây là hàm sử dụng để tăng state number lên 1 nên trong suốt quá trình thực hiện hàm không cần phải thay đổi gì cả => dependencies = []

Kết quả:

Ta nhận thấy ngoài lần đầu Content được monnted vào DOM thì mỗi lần click component Content không hề bị re-render nữa.

Lưu ý

  • Không nên lạm dụng useCallback
  • Nếu bạn không sử dụng React memo cho component con thì ở component cha bạn không cần phải sử dụng useCallback làm gì cả. Bởi vì không có memo thì mỗi lần component cha re-render thì component con cũng sẽ bị re-render theo.

3. Hook useMemo

useMemo cũng giống như khái niệm React memo, nhưng có sự khác biệt rõ ràng. Nếu như React memo sinh ra với mục đích tránh việc re-render nhiều lần thì useMemo tránh cho việc tính toán lại một function lặp đi lặp lại nhiều lần mỗi lần component re-render.
Bản chất useMemo là caching lại giá trị return của function, mỗi lần component re-render nó sẽ kiểm tra giá trị tham số truyền vào function nếu giá trị đó không thay đổi, thì return value đã caching trong memory. Ngược lại nếu giá trị tham số truyền vào thay đổi, nó sẽ thực hiện tính toán lại vào trả về value, sao đó caching lại value cho những lần re-render tiếp theo.

Chúng ta xét ví dụ sau

import { useState } from "react"
 
function App() {
 
  const [number, setNumber] = useState("")
  const [numbers, setNumbers] = useState([])
 
  const handleClick = () => {
    setNumbers(prevState => [...prevState, parseInt(number)])
  }
 
  const total = numbers.reduce((number, total) => {
    console.log('re-render...')
    return number + total
  }, 0)
 
  return (
    <div>
      <input
        value={number}
        onChange={e => setNumber(e.target.value)}
      />
      <button onClick={handleClick}>Click me!</button>
      <h1>Total: {total}</h1>
      <ul>
        {numbers.map((number, index) => (
          <li key={index}>{number}</li>
        ))}
      </ul>
    </div>
  )
}
 
export default App

Sau mỗi lần mình nhập 1 số vào ô input và click vào button click me! thì sẽ hiển thị giá trị vừa nhập ở list danh sách đồng thời cập nhật lại giá trị Total.

Kết quả


Vấn đề ở đây là khi ta nhập giá trị cho ô input thì component sẽ bị re-render đồng thời total cũng được tính lại. Chúng ta chỉ muốn cập nhật lại total khi click vào button nên việc re-render này là không cần thiết.

Ứng dụng useMemo

import { useMemo, useState } from "react"

function App() {

  const [number, setNumber] = useState("")
  const [numbers, setNumbers] = useState([])

  const handleClick = () => {
    setNumbers(prevState => [...prevState, parseInt(number)])
  }

  const total = useMemo(() => {
    console.log('re-render...')
    return numbers.reduce((number, total) => {
      return number + total
    }, 0)
  }, [numbers])

  return (
    <div>
      <input
        value={number}
        onChange={e => setNumber(e.target.value)}
      />
      <button onClick={handleClick}>Click me!</button>
      <h1>Total: {total}</h1>
      <ul>
        {numbers.map((number, index) => (
          <li key={index}>{number}</li>
        ))}
      </ul>
    </div>
  )
}

export default App

Kết quả


Ta thấy biến total chỉ bị tính lại khi mình click vào button click me! mà thôi.

4. So sánh sự khác nhau useMemo và useCallback

  • useMemo giữ cho một hàm không được thực thi lại nếu nó không nhận được một tập hợp các tham số đã được sử dụng trước đó. Nó sẽ trả về kết quả của một function. Sử dụng nó khi bạn muốn ngăn một số thao tác nặng hoặc tốn kém tài nguyên được gọi trên mỗi lần render.
  • useCallback giữ cho một hàm không được tạo lại lần nữa, dựa trên mảng các phần phụ thuộc. Nó sẽ trả về chính function đó. Sử dụng nó khi mà bạn muốn truyền fuction vào component con và chặn không cho một hàm nào đó tiêu thời gian, tài nguyên phải tạo lại.

Tổng kết

Qua một số chia sẻ trên của mình, hi vọng các bạn có thêm cái nhìn về React memo cũng như 2 Hooks của React là useCallbackuseMemo.
Rất mong nhận được ý kiến đóng góp của mọi người để mình có thể hoàn thiện bài viết này hơn. Chúc mọi người có một ngày làm việc hiệu quả !!!

Tài liệu tham khảo