📋 Mục lục
useState– Nền tảng của trạng tháiuseEffect– Các hiệu ứng phụ và vòng đờiuseContext– Chia sẻ trạng thái mà không cần propsuseReducer– Logic trạng thái phức tạp và có thể dự đoánuseRef– Tham chiếu, Lưu trữ và ThoátuseMemovàuseCallback– Tối ưu hóa hiệu suất- Custom Hooks – Tạo logic tái sử dụng
- Lỗi thông thường và giải pháp
8. Lỗi thông thường và giải pháp
Vấn đề: Re-render không cần thiết và sự so sánh tham chiếu
Trước khi hiểu giải pháp, điều quan trọng là phải hiểu vấn đề. Trong JavaScript, các đối tượng và hàm được so sánh bằng tham chiếu, không phải giá trị.
javascript
// Mặc dù có cùng "giá trị", nhưng chúng là hai đối tượng khác nhau trong bộ nhớ.
{} === {} // false
// Cũng tương tự với các hàm.
(() => {}) === (() => {}) // false
Trong React, mỗi khi một thành phần được re-render, các hàm và đối tượng bên trong nó được tạo lại. Điều này có nghĩa là các tham chiếu của chúng thay đổi trong mỗi lần render.
Tại sao đây là một vấn đề? Nếu bạn truyền một trong những hàm hoặc đối tượng này như một prop cho một thành phần con được tối ưu hóa bằng React.memo, thành phần con sẽ vẫn được re-render, bởi vì từ quan điểm của nó, prop (onSomething) đã thay đổi.
Đây là lúc useMemo và useCallback phát huy tác dụng. Chúng là công cụ để nói với React: "Đừng tạo lại cái này trừ khi các phụ thuộc của nó thay đổi".
useMemo: Nhớ các giá trị đã tính toán
useMemo "nhớ" (cache) kết quả của một phép tính tốn kém. Hàm mà bạn truyền vào chỉ được thực thi lại nếu một trong các phụ thuộc đã thay đổi.
Sử dụng để:
- Các phép tính tốn kém về mặt tính toán (ví dụ: lọc, sắp xếp hoặc biến đổi các danh sách lớn).
- Tránh việc tạo ra các đối tượng/mảng mới trong mỗi lần render nếu chúng được truyền như props cho các thành phần được tối ưu hóa.
Cú pháp:
javascript
const giaTriNho = useMemo(() => tinhToanTonKem(a, b), [a, b]);
useCallback: Nhớ các hàm
useCallback tương tự như useMemo, nhưng thay vì nhớ kết quả của một hàm, nó nhớ định nghĩa của hàm.
Sử dụng để:
- Tránh re-render trong các thành phần con được tối ưu hóa (
React.memo) nhận hàm như props. - Là một phụ thuộc ổn định trong các hooks khác như
useEffect.
Cú pháp:
javascript
const hamNho = useCallback(() => {
thucHienCongViec(a, b);
}, [a, b]);
Về mặt khái niệm, nó tương đương với: useMemo(() => () => { thucHienCongViec(a, b); }, [a, b]).
Ví dụ thực tế chi tiết: danh sách người dùng có thể lọc
Hãy tưởng tượng một danh sách người dùng mà bạn có thể lọc. Việc lọc có thể chậm nếu danh sách lớn. Hơn nữa, mỗi mục trong danh sách có một nút để xóa nó.
Thành phần UserItem (Đã tối ưu hóa)
javascript
import React from 'react';
// React.memo tránh việc thành phần này được re-render nếu props không thay đổi.
const UserItem = React.memo(({ user, onDelete }) => {
console.log(`Render UserItem cho: ${user.name}`);
return (
<li>
{user.name}
<button onClick={() => onDelete(user.id)}>Xóa</button>
</li>
);
});
Thành phần Chính UserList
javascript
import React, { useState, useMemo, useCallback } from 'react';
const danhSachNguoiDung = [
{ id: 1, name: 'Ana' },
{ id: 2, name: 'Carlos' },
{ id: 3, name: 'Beatriz' },
{ id: 4, name: 'Daniel' },
];
function UserList() {
const [users, setUsers] = useState(danhSachNguoiDung);
const [filter, setFilter] = useState('');
const [theme, setTheme] = useState('light'); // Trạng thái thêm để buộc re-render
// 1. Tối ưu hóa tính toán lọc với `useMemo`
// Hàm này chỉ được thực thi lại nếu `users` hoặc `filter` thay đổi,
// không khi `theme` thay đổi.
const filteredUsers = useMemo(() => {
console.log('Đang lọc người dùng...');
return users.filter(user =>
user.name.toLowerCase().includes(filter.toLowerCase())
);
}, [users, filter]);
// 2. Tối ưu hóa hàm xóa với `useCallback`
// `handleDelete` giữ cùng một tham chiếu trong khi `users` không thay đổi.
// Điều này tránh việc tất cả `UserItem` được re-render khi chúng ta thay đổi theme hoặc filter.
const handleDelete = useCallback((userId) => {
setUsers(prevUsers => prevUsers.filter(user => user.id !== userId));
}, []); // Phụ thuộc trống vì chúng tôi sử dụng cập nhật chức năng của `setUsers`
return (
<div style={{ background: theme === 'light' ? 'white' : 'black', color: theme === 'light' ? 'black' : 'white' }}>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Thay đổi Chủ đề
</button>
<hr />
<input
type="text"
placeholder="Lọc người dùng..."
value={filter}
onChange={e => setFilter(e.target.value)}
/>
<ul>
{filteredUsers.map(user => (
<UserItem key={user.id} user={user} onDelete={handleDelete} />
))}
</ul>
</div>
);
}
✅ Thực hành tốt và các mẫu thông thường
- ĐỪNG tối ưu hóa quá sớm:
useMemovàuseCallbackcó chi phí. Sử dụng chúng ở mọi nơi có thể làm ứng dụng của bạn chậm hơn. Chỉ áp dụng khi bạn đã xác định một vấn đề hiệu suất thực sự (có thể sử dụng Profiler của React DevTools). React.memolà bạn tốt nhất của bạn:useCallbackkhông làm gì nếu thành phần nhận hàm không được bọc trongReact.memo(hoặcPureComponent).- Phụ thuộc, phụ thuộc, phụ thuộc: Mảng phụ thuộc là chìa khóa. Đảm bảo bao gồm tất cả các giá trị trong phạm vi của thành phần mà bạn sử dụng bên trong hàm đã nhớ. Linter của React sẽ giúp bạn với điều này.
🚨 Lỗi thông thường và cách tránh chúng
-
Lỗi: Sử dụng
useMemođể nhớ các thành phần.- Giải pháp: Để nhớ một thành phần, hãy bọc nó trong
React.memo.useMemolà cho các giá trị, không cho JSX.
- Giải pháp: Để nhớ một thành phần, hãy bọc nó trong
-
Lỗi: Quên mảng phụ thuộc.
- Giải pháp: Nếu bạn bỏ qua mảng, hàm sẽ được tính toán lại trong mỗi lần render, làm cho việc tối ưu hóa trở nên vô ích. Nếu bạn muốn nó chỉ được tính toán một lần, hãy sử dụng
[].
- Giải pháp: Nếu bạn bỏ qua mảng, hàm sẽ được tính toán lại trong mỗi lần render, làm cho việc tối ưu hóa trở nên vô ích. Nếu bạn muốn nó chỉ được tính toán một lần, hãy sử dụng
-
Lỗi: Truyền một hàm được tạo trong thân thành phần vào phụ thuộc của
useEffect.javascript// SAI ❌ function MyComponent() { const fetchData = () => { /* ... */ }; useEffect(() => { fetchData(); }, [fetchData]); // `fetchData` là mới trong mỗi render -> vòng lặp vô tận }- Giải pháp: Định nghĩa hàm bên trong
useEffectnếu chỉ sử dụng ở đó, hoặc bọc nó tronguseCallbacknếu sử dụng ở nhiều nơi.
javascript// ĐÚNG ✅ const fetchData = useCallback(() => { /* ... */ }, []); useEffect(() => { fetchData(); }, [fetchData]); - Giải pháp: Định nghĩa hàm bên trong
🚀 Thử thách thực tiễn
- Tính giai thừa: Tạo một thành phần với một input số. Tính và hiển thị giai thừa của số đó. Việc tính toán phải được nhớ bằng
useMemo. Thêm một trạng thái khác (như một toggle theme) để kiểm tra xem giai thừa có được tính lại không cần thiết hay không. - Danh sách con với
React.memo: Tạo một thành phần cha render một danh sách 100 thành phần con. Truyền một hàmonPresscho con. Kiểm tra với mộtconsole.logbao nhiêu con được re-render khi cha thay đổi trạng thái của nó. Sau đó, tối ưu hóa vớiuseCallbackvàReact.memovà kiểm tra sự khác biệt. - Đồ thị SVG phức tạp: Giả lập một thành phần render một đồ thị SVG (có thể tốn kém). Các tọa độ của đồ thị được tính toán từ một mảng dữ liệu. Sử dụng
useMemođể nhớ các tọa độ đã tính toán và tránh tính toán lại nếu các props khác của thành phần thay đổi.