Cảm ơn bạn!
Cách sử dụng React Context hiệu quả tránh các re-render component không cần thiết
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ụnguseState
hoặcuseReducer
.
Để 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 useMemo
và React.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 MovieContext
và UserContext
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 nickname
và changeNickname
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 />
và <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 ^^.