Trong 2 bài trước chúng ta đã cùng nhau tìm hiểu về hàm useState, useRef
và useReducer
trong bài này Dũng sẽ cùng các bạn tìm hiểu về các hàm useCallback
nhes nhé.
Vấn đề
Hãy nói chúng ta có một lớp component có tên Tasks thế này:
import React, { memo } from 'react';
interface TasksProps {
tasks: string[];
addTask: () => void;
}
const Tasks: React.FC<TasksProps> = ({ tasks, addTask }) => {
console.log("tasks render");
return (
<>
<h2>My tasks</h2>
{tasks.map((task, index) => {
return <p key={index}>{task}</p>;
})}
<button onClick={addTask}>Add task</button>
</>
);
};
export default memo(Tasks);
Và mã nguồn của App.tsx sẽ thay đổi như sau:
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import Tasks from './Tasks';
function App() {
const [count, setCount] = useState(0);
const [tasks, setTasks] = useState<string[]>([]);
console.log("1. init");
const addTask = () => {
setTasks((t) => [...t, "New Task"]);
};
console.log("2. render");
return (
<>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<Tasks tasks={tasks} addTask={addTask} />
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
}
export default App
Chúng ta kỳ vọng rằng mỗi lần chúng ta nhấn nút count is thì sum sẽ tăng lên và Tasks sẽ không bị render lại do chúng ta đã sử dụng memo(Tasks).
Tuy nhiên thực tế kết quả sẽ như sau:
Như bạn có thể thấy ở phần console, khi chúng ta nhấn vào nút count is thì vẫn có log tasks render được in ra điều này có nghĩa là Tasks vẫn được render lại. Nguyên nhân là do mỗi lần state thay đổi App sẽ được render lại nên hàm addTask cũng bị tạo lại, nghĩa là làm cho reference được truyền vào props của Tasks bị thay đổi dẫn đến nó cũng bị render lại.
Sử dụng useCallback
Để giải quyết vấn đề trên chúng ta có thể sử dụng useCallback
và mã nguồn của App.tsx như sau:
import { useCallback, useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import Tasks from './Tasks';
function App() {
const [count, setCount] = useState(0);
const [tasks, setTasks] = useState<string[]>([]);
console.log("1. init");
const addTask = useCallback(() => {
setTasks((t) => [...t, "New Task"]);
}, []);
console.log("2. render");
return (
<>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<Tasks tasks={tasks} addTask={addTask} />
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
}
export default App
Ở đây chúng chỉ thay đổi đoạn mã từ:
const addTask = () => {
setTasks((t) => [...t, "New Task"]);
};
Sang:
const addTask = useCallback(() => {
setTasks((t) => [...t, "New Task"]);
}, []);
Bây giờ bạn có thể quay trở lại http://localhost:5173/ và nhấn vào nút count is và không thấy có log tasks render được in ra nữa, nghĩa là Tasks đã không được render lại.
Chỉ khi bạn nhấn nut Add task thì log tasks render, Tasks được render lại và có thêm New Task mới được hiện thị ra.
Tại sao useCallback lại ngăn chặn được việc render lại Tasks? Điều kỳ diệu đến từ khả năng ghi nhớ của nó.
Khi không có lời gọi nào vào hàm useCallback mà cụ thể ở đây là hàm addTask thì hàm lambda bên trong useCallback sẽ không thay đổi dẫn đến Tasks cũng sẽ không bị render lại.
Ngoài ra, ví dụ chúng ta bổ sung biến trạng thái count vào phụ thuộc của hàm useCallback như sau:
const addTask = useCallback(() => {
setTasks((t) => [...t, "New Task"]);
}, [count]);
Trong các lần render tiếp theo, React sẽ so sánh các phụ thuộc với các phụ thuộc bạn đã truyền trong lần render trước. Nếu không có phụ thuộc nào thay đổi (sử dụng hàm Object.is), useCallback sẽ trả về cùng một hàm như trước. Nếu không, useCallback sẽ trả về hàm bạn đã truyền trong lần render mới.
Nói cách khác, useCallback lưu trữ một hàm giữa các lần render cho đến khi các phụ thuộc của nó thay đổi.
Tổng kết
Như vậy chúng ta đã cùng nhau tìm hiểu về hàm useCallback các hàm còn lại chúng ta sẽ tìm hiểu trong bài sau nhé.
Cám ơn bạn đã quan tâm đến bài viết này. Để nhận được thêm các kiến thức bổ ích bạn có thể:
- Đọc các bài viết của TechMaster trên facebook: https://www.facebook.com/techmastervn
- Xem các video của TechMaster qua Youtube: https://www.youtube.com/@TechMasterVietnam nếu bạn thấy video/bài viết hay bạn có thể theo dõi kênh của TechMaster để nhận được thông báo về các video mới nhất nhé.
- Chat với techmaster qua Discord: https://discord.gg/yQjRTFXb7a
Bình luận