Tại sao React component của tôi re-render 2 lần?

Tại sao React component của tôi re-render 2 lần?

Bài viết này Được sẽ lý giải một số trường hợp component React của bạn bị re-render 2 lần dù bạn đã kiểm tra kĩ lắm rồi nhưng vẫn không phát hiện lỗi sai.

Trước tiên mình sẽ liệt kê một số lý do làm component React của các bạn bị re-render


🥇 Những trường hợp làm React Component Re-render

  • Cập nhật state mới cho component

  • Thay đổi props component

  • Thay đổi url khi sử dụng router

  • Component cha re-render dẫn đến component con re-render

Đó là những trường hợp phổ biến nhất mà ai cũng đều biết. Chúng ta có thể khắc phục bằng cách dùng useMemo, useCallback, hay React.memo.

Bài viết này mình không đề cấp đến những cái trên mà là việc

  • callback trong useEffect chạy 2 lần do dùng React Strict Mode

  • Cập nhật state giống state cũ nhưng component vẫn re-render


🥇 Sử dụng React Strict Mode làm component bị mount và unmount

Đây là trường hợp phổ biến và hay gặp nhất, nhiều bạn mới code React không hiểu tại sao lại thêm cái Strict Mode này vào làm gì để cho việc kiểm soát re-render của các bạn bị rối.

Đúng là thoạt nhìn qua thì thấy rối thật, nó làm component của các bạn mount (khởi tạo) rồi lại unmount (hủy)

Dẫn đến code xử lý trong component bị chạy 2 lần (hãy xem đoạn code console.count("Render");) và callback trong useEffect bị gọi 2 lần (xem đoạn code console.count("Số lần Callback trong useEffect chạy");).

Nhìn ví dụ dưới đây

Tất cả lý do trên đều do React Strict Mode gây nên, nếu bạn tắt React Strict Mode thì sẽ không còn gặp hiện tượng đó nữa. Và React Strict Mode chỉ hoạt động ở môi trường dev thôi, khi build ra production thì nó không hoạt động.

🥈 Vậy tác dụng của React Strict Mode là gì?

Như cái tên gọi của nó, đây là "Chế độ an toàn cho React", nó sẽ giả sử các trường hợp có thể xảy ra trong thực tế khi ứng dụng bạn chạy

Ví dụ mình gọi API trong useEffect() như thế này thì API sẽ bị gọi 2 lần ngay lần đầu app mình chạy

Books.jsx

function Books() {
  const [books, setBooks] = useState([])
  useEffect(() => {
    getBooks().then((res) => {
      setBooks(res)
    })
  }, [])
  return <div>Books</div>
}

App.jsx

import Books from './Books'
export default function App() {
  return (
    <div className="App">
      <Books />
    </div>
  )
}

Ý React Strict Mode muốn nói ở đây là trong thực tế cái API đó có thể bị gọi liên tiếp 2 lần và có thể gây ảnh hưởng đến ứng dụng của bạn.

Lúc này bạn nghĩ "Làm éo gì có chuyện gọi 2 lần API như việc Strict Mode mô phỏng 🤣"

Vậy nếu bây giờ component App của các bạn như thế này và bạn nhanh tay ấn liên tục button thì component Books của bạn sẽ bị mount / unmount liên tục dẫn đến API gọi liên tục.

import Books from './Books'
export default function App() {
  const [visible, setVisible] = useState(true)
  return (
    <div className="App">
      <button onClick={() => setVisible((prev) => !prev)}>Ẩn hiện Books</button>
      {visible && <Books />}
    </div>
  )
}

Và trường hợp có thể xảy ra là khi component Books bị unmount và sau đó thì api gọi xong, nó tiến hành setBooks(res) ở một component đã bị hủy 🤣

Đấy! Thực tế điều này vẫn có thể xảy ra.

Vậy khi chúng ta thấy gọi API 2 lần ở useEffect thì chúng ta nên sử dụng clean up function cho useEffect

Books.jsx

function Books() {
  const [books, setBooks] = useState([])
  useEffect(() => {
    getBooks().then((res) => {
      setBooks(res)
    })
    return () => {
      // Cancel Request tại đây!
      // controller.abort()
    }
  }, [])
  return <div></div>
}

Cancel request bằng AbortController đều có sẵn trên AxiosFetch API

Nếu các bạn lắng nghe sự kiện trong useEffect thì cũng nhớ nên hủy việc lắng nghe trong clean up function nhé

Nếu anh em dùng React Query thì nó xử lý giúp chúng ta mấy cái việc lúc nào cũng phải nhớ dùng clean up cho các sự kiện gọi api. Rất tiện luôn. Đó là lý do mình đã chuyển sang dùng React Query.


🥇 Component re-render 2 lần dù setState cùng giá trị giống nhau

Điều kiện để component của bạn re-render khi dùng setState là chúng ta phải setState với giá trị khác với state hiện tại (React sử dụng thuật toán so sánh Object.is())

  • Đối với kiểu dữ liệu nguyên thuỷ thì khác giá trị

  • Đối với object thì khác tham chiếu

Nhưng ta sẽ gặp một trường hợp như dưới đây.

Khi nhấn button lần đầu tiên thì re-render 1 lần (Mary thành Phạm Hồng Đức)

Nhấn lần thứ 2 thì re-render lần thứ nữa mặc dù 2 giá trị trước và sau không thay đổi (Phạm Hồng Đức thành Phạm Hồng Đức)

Nhấn lần thứ 3 thì không còn thấy hiện tượng re-render nữa.

export default function App() {
  const [name, setName] = useState('Mary')
  return (
    <div>
      <button onClick={() => setName('Phạm Hồng Đức')}>Change Name</button>
      <div>{name}</div>
    </div>
  )
}

Các bạn xem demo dưới dây, mình không dùng React Strict Mode trong trường hợp này để các bạn đỡ rối khi nhìn vào console log

Theo như Team React giải thích tại sao component re-render 2 lần dùng setState cùng giá trị là

  • Khi bạn nhấn button ở lần 2, React sẽ không biết liệu bạn có thực sự muốn set state và re-render hay không nên React sẽ re-render.

  • Ở lần nhấn button thứ 3, khi chúng ta lại set với trị cũ thì bây giờ React sẽ không re-render nữa, vì cả 2 state đều giống nhau.


🥇 Tóm lại

Các bạn thấy đó, mọi chuyện trên đời đều có lý do của nó cả 😁, hy vọng bài viết này có thể giải đáp được các thắc mắc của các bạn bấy lâu nay.

Nguồn bài viết: https://duthanhduoc.com

Phạm Hồng Đức
Phạm Hồng Đức
Một developer thích nghiên cứu và chia sẻ kiến thức lập trình, phần mềm mã nguồn mở (open-source), và cuộc sống.