useCallback
useCallback được sử dụng để tối ưu quá trình render của React functional components.
Nó sẽ rất hữu ích đối với trường hợp một component liên tục được hiển thị lại không cần thiết trong quá trình xử lý và có hành vi chức năng phức tạp.
Hãy nhớ rằng React đã rất nhanh, tối ưu hiệu xuất chỉ nên sử dụng cho những component có khả năng chậm, xử lý tác vụ nặng. Khi đó, chúng ta sẽ xem xét xử dụng useCallback làm một phần hỗ trợ tối ưu hiện xuất trong các hook.
Cấu trúc
const increase = useCallback(() => setCount(count + 1), [count]);
trong đó:
() => setCount(count + 1)
: là tham số thứ 1, nó là 1 function sẽ được ghi nhớ bởi useCallback[count]
: danh sách phụ thuộc, khi có 1 phần tử trong danh sách bị thay đổi giá trị, thì function sẽ được thực thi lại
Trong lần đầu component được render, useCallback sẽ ghi nhớ hàm được truyền vào nó.
Với mỗi lần render tiếp theo, useCallback sẽ thực hiện phép so sánh tham chiếu bằng hàm Object.is
để đối chiếu sự thay đổi của các dependencies truyền vào nó, nếu không có thay đổi, hàm truyền vào sẽ hoàn toàn bị bỏ qua.
Khi có sự thay đổi dependencies, useCallback sẽ thay thế hàm bạn đã truyền vào nó thay cho hàm đã lưu trước đó.
Dựa trên hoạt động của useCallback , chúng ta có thể thấy là trên thực tế nó phải làm một số nhiệm vụ là ghi nhớ, so sánh và trả về hàm callback.
cách thức hoạt động của useCallback
Khi truyền một hàm inline callback và dependencies, useCallback sẽ trả lại một phiên bản của hàm callback đã được ghi nhớ và hàm đó chỉ thay đổi nếu một trong những dependencies đã bị thay đổi.
Việc này hữu dụng khi truyền các hàm callback vào các component con đã tối ưu dựa trên reference equality để ngăn các render không cần thiết
Bình đẳng tham chiếu trong JavaScript được biết như sau (react dùng Object.is
nhưng dùng ===
cũng kết quả tương tự)
true === true // true
false === false // true
1 === 1 // true
'a' === 'a' // true
{} === {} // false
[] === [] // false
function foo() {return 0 }
function bar() { return 0 }
foo === bar; // false
foo === foo; // true
Khi nào không nên sử dụng useCallback
Khi nghĩ về cách nâng cấp hiệu xuất như sử dụng useCallback, luôn phải đo lường tốc độ của các component của bạn trước khi bắt đầu tối ưu hoá chúng. (dùng profiler react để đo hiệu suất)
Mục đích của useCallback là giúp chúng ta tối ưu hiệu suất. Nhưng sử dụng useCallback một cách hiệu quả là câu chuyện khác.
Trong React, khi một react function component, nếu nó có chứa một hàm bên trong nó, thì hàm đó sẽ render mỗi lần component đó render.
Đây là một điều bình thường của React, phần lớn các trường hợp thì việc tái tạo lại dăm ba cái inline function lẻ tẻ không ảnh hưởng gì mấy đến hiệu suất khi render, vì vậy việc re-render đó hoàn toàn chấp nhận được.
useCallback cũng sỡ hữu nhược điểm, chủ yếu là độ phức tạp của code.
Ở đây có rất nhiều trường hợp không hợp lý khi thêm useCallback và chúng ta phải chấp nhận để hàm khởi tạo lại.
Như đã nói, useCallback cũng sở hữu nhược điểm về hiệu xuất, vì nó vẫn phải chạy trên mọi lần re-render, sau đây là 1 ví dụ mà sử dụng useCallback thậm chí còn chậm hơn.
ví dụ
function CandyDispenser() { const initialCandies = ['snickers', 'skittles', 'twix', 'milky way'] const [candies, setCandies] = React.useState(initialCandies) const dispense = candy => { setCandies(allCandies => allCandies.filter(c => c !== candy)) } return (<div> <h1>Candy Dispenser</h1> <div> <div>Available Candy</div> {candies.length === 0 ? (<button onClick={() => setCandies(initialCandies)}>refill</button>) : (<ul> {candies.map(candy => (<li key={candy}><button onClick={() => dispense(candy)}>grab</button> {candy}</li>))} </ul>)} </div> </div>);
}
Trong ví dụ này, điều duy nhất thay đổi là bọc dispense
bằng useCallback
:
const dispense = React.useCallback(candy => { setCandies(allCandies => allCandies.filter(c => c !== candy))
}, []);
Giờ nhìn lại bản gốc:
const dispense = candy => { setCandies(allCandies => allCandies.filter(c => c !== candy))
}
Không, tôi chả thấy vấn đề gì cả.
Hãy nhớ rằng, mỗi dòng code đều có chi phí, giờ hãy code lại useCallback một chút, không có thay đổi thực sự, chỉ để minh họa rõ hơn:
const dispense = candy => { setCandies(allCandies => allCandies.filter(c => c !== candy))
};
const dispenseCallback = React.useCallback(dispense, []);
Và đây là bản gốc một lần nữa:
const dispense = candy => { setCandies(allCandies => allCandies.filter(c => c !== candy))
}
Và sau đây là phân tích chi tiết tại sao useCallback lại tệ hơn trong trường hợp này:
Hãy nhớ rằng useCallback đang phải lưu lại dispense
tại một vùng nhớ nào đó, đồng thời khai báo thêm 1 depend List ([])
Và bạn đang gọi React.useCallback
để bắt nó so sánh giữa 2 mảng này, không có depend nào, nhưng vẫn là logic so sánh.
Tại lần render tiếp theo, sẽ có 2 dispense
đồng thời tồn tại, 1 được lưu lại bởi useCallback và 1 cái mới tạo ra bằng lambda.
Sau khi useCallback so sánh xong, không có depend nào bị thay đổi cả, dispense
tạo ra bằng lambda sẽ bị xóa.
Đây là trường hợp depend List rỗng ([])
, nó có giá trị nghĩa là các giá trị này còn được lưu trữ ở đâu đó nữa.
Vậy khi nào nên dùng useCallback
Trường hợp bắt buộc phải sử dụng useCallback là khi TRUYỀN các hàm callback vào các Component Con đã tối ưu.
Một số trường hợp khác cũng lên quan đến việc render thông quan các hàm callback:
- nó được bọc bên trong React.memo()
- nó nằm trong dependency list của các Hook khác, vd: useEffect(..., [callback])
Tối ưu hóa hiệu suất không miễn phí. Chúng LUÔN đi kèm với chi phí nhưng KHÔNG phải lúc nào cũng đi kèm với lợi ích để bù đắp chi phí đó.
Do đó, hãy tối ưu hóa một cách có trách nhiệm .
Kết luận
- useCallback được sử dụng để tránh tạo mới 1 callback function
- Nó thường được sử dụng trong các trường hợp cần truyền function xuống Component Con hoặc truyền vào trong Depend list
- Nó thường kết hợp với React.memo và tận dụng shallowly compare bên trong memo để tránh re-render
- cũng như React.memo, useCallback cũng tốn thêm tài nguyên khi sử dụng, nên cũng cần chú ý khi dùng