TABLE OF CONTENTS

Hi 🤓 Cảm ơn bạn đã ghé thăm blog này, nếu những bài viết trên blog giúp ích cho bạn. Bạn có thể giúp blog hiển thị quảng cáo bằng cách tạm ngừng ad blocker 😫 và để giúp blog duy trì hoạt động nếu bạn muốn.
Cảm ơn bạn!

Trong quá trình làm quen với React Context, chắc hẳn một số bạn đã cảm thấy khó hiểu khi component re-render nhiều lần, các lần re-render này có thể là không cần thiết và nó ít nhiều gây ra vấn đề performance cho trang web của chúng ta 🐻.

Làm thế nào để hạn chế re-render component khi sử dụng React Context? Trong bài viết này chúng ta cùng tìm hiểu về nguyên nhân và cách khắc phục nhé! Bài viết dựa trên kinh nghiệm của mình nên có gì sai sót mong các bạn bỏ qua nhé ^^.

React Context

React Context cho phép chúng ta truyền và sử dụng data trong các component mà không cần sử dụng props. Giả sử chúng ta muốn có một global state để quản lý thông tin user. Thay vì truyền data bằng props thông qua nhiều tầng component để đến được component muốn sử dụng data, ta chỉ cần sử dụng React Context, giờ đây việc lấy data trở nên rất nhanh chóng và dễ dàng. Các bạn có thể tìm hiểu thêm tại đây: Context - React.

Để tìm hiểu nguyên nhân component bị re-render nhiều lần trong quá trình sử dụng React Context, ta cùng xem một ví dụ nhé!

Giả sử mình có một state quản lí các thông tin cho một web xem phim như: User, Movies.

Để sử dụng React Context chúng ta thực hiện 3 bước sau: Tạo context, wrap các component muốn sử dụng data vào provider, và sử dụng context.

Để tạo context ta sử dụng createContext của React, đối số của createContext là default value.

import { createContext } from 'react';
const DemoContext = createContext({
  username:"", 
  nickname:"",
  movies: []
});

Để sử dụng data từ global state, chúng ta sẽ sử dụng DemoContext.Provider với prop là value và truyền giá trị vào.

export default function App() {
  const [nickname, setNickname] = useState("Mjnh2k");
  const [movies] = useState([
    "Spider-Man: No Way Home",
    "Stand by Me Doraemon 2",
    "The Matrix Resurrections"
  ]);

  const changeNickname = () => setNickname("Bojdatjnk2k");
  const value = { nickname, changeNickname, movies }
  console.log("render <App/>");

  return (
    <div className="App">
      <DemoContext.Provider value={value}>
        <Header />
        <Movies />
      </DemoContext.Provider>
    </div>
  );
}

Ở trên ta truyền <Header />, <Movies /> vào <DemoContext.Provider>, lúc này ta mới có thể sử dụng value ở trên.

Nếu ta bỏ <Header />, <Movies /> ngoài <DemoContext.Provider> thì ta chỉ có thể sử dụng giá trị default, chúng ta cũng không thể làm component re-render khi thay đổi giá trị default. Nếu muốn update context value và re-render component ta sẽ phải sử dụng useState hoặc useReducer.

Để sử dụng data thì ta chỉ cần sử dụng useContext như sau:

function Header() {
  return <header><UserInfo /></header>
}

function UserInfo() {
  const { nickname, changeNickname } = useContext(UserContext);

  console.log("render <UserInfo/>");
  return (
    <div>
      <button onClick={changeNickname}>Change Nickname</button>
      <b>{nickname}</b>
    </div>
  );
}

<UserInfo/> ta sử dụng data nickname, changeNickname. Để update nickname thì ta sử dụng changeNickname đã tạo từ trước.

Khi click vào changeNickname, nickname sẽ thay đổi dẫn đến <App /> re-render.

Lúc này context value đã thay đổi vì một giá trị mới đã truyền vào value (<DemoContext.Provider value={value}>). Bất cứ component nào sử dụng useContext sẽ luôn re-render khi context value thay đổi.

Trong <Movies /> chúng ta chỉ sử dụng giá trị movies chứ không sử dụng nickname cho nên việc re-render là không cần thiết 👍.

Chúng ta sẽ xử lý việc hạn chế re-render <Movies /> sau 😅.

Một trường hợp re-render các component ở trên nữa đó là giá trị truyền vào value của <DemoContext.Provider value={value}>. Giả sử chúng ta re-render lại <App /> khi thay đổi một state bất kỳ mà không liên quan đến context value.

function App() {
  const [nickname, setNickname] = useState("Mjnh2k");
  const [movies] = useState(["Spider-Man: No Way Home"]);
  const [time, setTime] = useState(0);

  const changeNickname = () => setNickname("Bojdatjnk2k");
  const value = { nickname, changeNickname, movies };

  return (
    <div className="App">
      <button onClick={() => setTime(time + 1)}>UPDATE TIME</button>
      <DemoContext.Provider value={value}>
        <Header />
        <Movies />
      </DemoContext.Provider>
      <Footer />
    </div>
  );
}

Nếu click vào button UPDATE TIME <App /> sẽ re-render, khi re-render thì value là một object mới kể cả các giá trị trong object này bạn thấy nó không thay đổi. Lý do là vì object này thuộc một địa chỉ ô nhớ mới mỗi lần <App /> re-render 👍.

Khi <App /> re-render thì giá trị truyền vào value của <DemoContext.Provider value={value}> là một object mới như mình đã nói ở trên, chính vì thế React sẽ trigger các consumer(Là các component sử dụng context value) re-render. Đây là lý do khi click vào button UPDATE TIME ta thấy <Header />, <Movies /> re-render lại.

Để giải quyết vấn đề trên thì ta sẽ ghi nhớ(memoizes) value nhằm ngăn re-render cho các consumer ở trên.

Chúng ta sẽ sử dụng React.useMemo như sau:

const value = useMemo(() => {
  return {userName, setUserName }, 
  [userName]
);

React.useMemo sẽ update lại object bên trong nó khi userName trong [userName] thay đổi. Nếu userName không đổi thì value lúc này sẽ sử dụng object nó đã ghi nhớ và không tạo ra reference mới ^^.

Chúng ta sử dụng useMemoReact.memo để hạn chế re-render không cần thiết cho các component. Các bạn có thể theo dõi video dưới đây để hiểu rõ hơn ^^.

Mình đã viết một bài về React.memo các bạn có thể đọc tại đây: React memo là gì? Hạn chế re-render component với React memo.

Như vậy là chúng ta đã giải quyết được vấn đề liên quan đến memoizes value.

Tiếp theo chúng ta sẽ tìm cách làm thế nào để ngăn <Movies /> re-render khi ta click vào button changeNickname. Lý do như ở trên mình đã nói đó là khi context value thay đổi nó sẽ trigger re-render các consumer ^^.

Việc re-render <Movies /> là không cần thiết và có một vài cách để chúng ta có thể ngăn component này re-render.

Các bạn có thể tham khảo tại đây: Preventing rerenders with React.memo and useContext hook.

Trong bài viết này mình sẽ sử dụng cách làm đó là chúng ta sẽ chia các context ra thay vì gộp chúng lại như DemoContext.

Chúng ta sẽ tách ra thành MovieContextUserContext như sau:

const MovieContext = createContext({ movies: [] });

const UserContext = createContext({
 name: "Hung",
 born: "2000",
 nickname: "Bojdatjnk",
 changeNickname: () => {}
});

Lý do chúng ta nên tách chúng ta vì khi chúng ta update context value của UserContext thì các consumer của MovieContext sẽ không bị re-render, lúc này chúng không đã không còn thuộc về nhau 🤦‍♂️😁.

<App /> của chúng ta giờ sẽ thế này ^^

function App() {
  const [nickname, setNickname] = useState("Trang");
  const [movies] = useState(["Spider-Man: No Way Home"]);

  const changeNickname = () => setNickname("Bojdatjnk2k");
  const valueUser = useMemo(() => ({ nickname, changeNickname }), [nickname]);
  // movies lúc này các bạn có thể không cần sử dụng useMemo
  // Vì chúng ta không thay đổi giá trị của nó, và nó đã được
  // ghi nhớ vào trong movies ở useState

  console.log("render <App/>");

  return (
    <div className="App">
      <UserContext.Provider value={valueUser}>
        <Header />
        <MovieContext.Provider value={movies}>
          <Ads />
          <Movies />
        </MovieContext.Provider>
      </UserContext.Provider>
      <Footer />
    </div>
  );
}

Trong <Header /> ta sẽ sử dụng nicknamechangeNickname từ UserContext, Trong <Ads /> ta sử dụng nickname, movies và component <Movies /> chỉ sử dụng movies từ MovieContext.

Lúc này nếu ta changeNickname từ <Header /> thì cả <Header /><Ads /> sẽ bị re-render, vì chúng có sử dụng giá trị nickname hay cả hai component này là consumer nên khi context value thay đổi thì chúng sẽ bị re-render.

Để ngăn re-render không cần thiết của <Movies /> ta chỉ cần sử dụng React.memo như ví dụ trước ^^.

const Movies = memo(function () {
  const movies = useContext(MovieContext);
  console.log("render <Movies/>");

  // ...
});

Các bạn có thể coi demo tại đây ^^.

Kết luận

Như vậy là chúng ta đã giải quyết xong vấn đề re-render ở trên, hi vọng bài viết giúp ích cho các bạn. Từ đây, các bạn có thể giải quyết được các vấn đề gặp phải liên quan đến việc re-render component với React Context.

Chúc các bạn học tốt ^^.

Có thể bạn thích ⚡
homiedev
About Me

Hi, I'm @devnav. Một người thích chia sẻ kiến thức, đặc biệt là về Frontend 🚀. Trang web này được tạo ra nhằm giúp các bạn học Frontend hiệu quả hơn 🎉😄.

Chúc các bạn tìm được kiến thức hữu ích trong blog này 😁😁.