javascript - useCallback trong React là gì và khi nào nên sử dụng nó?
Tôi đã đọc qua một vài bài viết về useCallback và useMemo về khi nào nên sử dụng và khi nào không nên sử dụng nhưng hầu hết tôi chỉ thấy các đoạn mã rất gượng ép. Tôi đang xem một đoạn mã tại công ty và nhận thấy ai đó đã làm như sau:
const takePhoto = useCallback(() => {
launchCamera({ mediaType: "photo", cameraType: "front" }, onPickImage);
}, []);
const pickPhotoFromLibrary = async () => {
launchImageLibrary({ mediaType: "photo" }, onPickImage);
}
const onUploadPress = useCallback(() => {
Alert.alert(
"Upload Photo",
"From where would you like to take your photo?",
[
{ text: "Camera", onPress: () => takePhoto() },
{ text: "Library", onPress: () => pickPhotoFromLibrary() },
]
);
}, [pickPhotoFromLibrary, takePhoto]);
Đây là cách onUploadPress được gọi:
<TouchableOpacity
style={styles.retakeButton}
onPress={onUploadPress}
>
Bạn có nghĩ đây là cách gọi đúng không? Dựa trên hiểu biết của tôi về những bài viết đó, điều này có vẻ không đúng. Có ai có thể cho tôi biết khi nào nên sử dụng useCallback và cũng có thể giải thích useCallback theo cách dễ hiểu hơn không?
Bài viết tôi đọc: Khi nào nên dùng useMemo và useCallback.
Câu trả lời #1
useCallback nhận tham số đầu tiên là một hàm và trả về một phiên bản được ghi nhớ của nó (về vị trí bộ nhớ, không phải tính toán bên trong). Có nghĩa là hàm trả về sẽ không được tạo lại trên một tham chiếu bộ nhớ mới mỗi khi component render lại, trong khi một hàm bình thường bên trong component thì có.
Hàm được trả về sẽ được tạo lại trên một tham chiếu bộ nhớ mới nếu một trong các biến bên trong mảng phần phụ thuộc của useCallback (tham số thứ hai) thay đổi.
Vậy, tại sao bạn lại muốn bận tâm đến điều này? Chà, điều đó đáng giá bất cứ khi nào hành vi bình thường của một hàm bên trong một thành phần lại gây ra vấn đề cho bạn.
Ví dụ, nếu bạn có hàm đó trong mảng phụ thuộc của một useEffect, hoặc nếu bạn truyền nó xuống một component được ghi nhớ bằng memo.
Hàm callback của
useEffectđược gọi khi lần render đầu tiên và mỗi khi một trong các biến trong mảng phụ thuộc của nó thay đổi. Và vì thông thường một phiên bản mới của hàm đó được tạo ra ở mỗi lần render, nên callback có thể bị gọi vô hạn. Do đóuseCallbackđược sử dụng để ghi nhớ nó.
Một component được ghi nhớ với
memochỉ re-render nếustatehoặcpropscủa nó thay đổi, không phải vì component cha re-render. Và vì thường một phiên bản mới của hàm được truyền dưới dạngpropsđược tạo ra, khi component cha re-render, component con nhận một tham chiếu mới, do đó nó re-render. Vì vậyuseCallbackđược sử dụng để ghi nhớ hàm đó.
Để minh họa, tôi đã tạo ứng dụng React hoạt động dưới đây. Nhấp vào nút đó để kích hoạt việc render lại của component cha và xem console. Hy vọng điều này sẽ làm rõ mọi thứ!
const MemoizedChildWithMemoizedFunctionInProps = React.memo(
({ memoizedDummyFunction }) => {
console.log("MemoizedChildWithMemoizedFunctionInProps renders");
return <div></div>;
}
);
const MemoizedChildWithNonMemoizedFunctionInProps = React.memo(
({ nonMemoizedDummyFunction }) => {
console.log("MemoizedChildWithNonMemoizedFunctionInProps renders");
return <div></div>;
}
);
const NonMemoizedChild = () => {
console.log("Non memoized child renders");
return <div></div>;
};
const Parent = () => {
const [state, setState] = React.useState(true);
const nonMemoizedFunction = () => {};
const memoizedFunction = React.useCallback(() => {}, []);
React.useEffect(() => {
console.log("useEffect callback with nonMemoizedFunction runs");
}, [nonMemoizedFunction]);
React.useEffect(() => {
console.log("useEffect callback with memoizedFunction runs");
}, [memoizedFunction]);
console.clear();
console.log("Parent renders");
return (
<div>
<button onClick={() => setState((prev) => !prev)}>Toggle state</button>
<MemoizedChildWithMemoizedFunctionInProps
memoizedFunction={memoizedFunction}
/>
<MemoizedChildWithNonMemoizedFunctionInProps
nonMemoizedFunction={nonMemoizedFunction}
/>
<NonMemoizedChild />
</div>
);
}
ReactDOM.render(
<Parent />,
document.getElementById("root")
);<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>Điều quan trọng là phải biết rằng việc ghi nhớ (memoizing) không miễn phí, vì vậy làm sai cách còn tệ hơn. Trong trường hợp của bạn, việc sử dụng useCallback cho onUploadPress là lãng phí vì một hàm không được ghi nhớ, pickPhotoFromLibrary, có trong mảng phụ thuộc. Ngoài ra, cũng là lãng phí nếu TouchableOpacity không được ghi nhớ với memo, điều mà tôi không chắc chắn.
Nhân tiện, có useMemo, hoạt động và được sử dụng giống như useCallback để ghi nhớ các giá trị không phải hàm nhưng được tham chiếu như objects và arrays vì cùng lý do, hoặc để ghi nhớ bất kỳ kết quả nào của một phép tính nặng mà bạn không muốn lặp lại giữa các lần render.
Một tài nguyên tuyệt vời để hiểu sâu về quy trình render của React, biết khi nào nên memoize và cách thực hiện tốt: React Render.
Câu trả lời #2
Nói một cách đơn giản, useCallback được sử dụng để lưu trữ tham chiếu của hàm ở đâu đó bên ngoài quá trình render của component để chúng ta có thể sử dụng lại cùng một tham chiếu. Tham chiếu đó sẽ được thay đổi bất cứ khi nào một trong các biến trong mảng dependencies thay đổi. Như bạn biết, React cố gắng giảm thiểu quá trình render lại bằng cách theo dõi sự thay đổi giá trị của một số biến, sau đó quyết định render lại mà không phụ thuộc vào giá trị cũ và giá trị mới của các biến đó. Vì vậy, cách sử dụng cơ bản của useCallback là giữ giá trị cũ và giá trị mới một cách tương đương.
Tôi sẽ cố gắng minh họa điều đó rõ hơn bằng cách đưa ra một số ví dụ trong các tình huống mà chúng ta phải sử dụng useCallback.
- Ví dụ 1: Khi hàm là một trong các phần tử của mảng phụ thuộc của
useEffect.
function Component(){
const [state, setState] = useState()
// Should use `useCallback`
function handleChange(input){
setState(...)
}
useEffect(()=>{
handleChange(...)
},[handleChange])
return ...
}
- Ví dụ 2: Khi hàm được truyền cho một trong các thành phần con. Đặc biệt khi nó được gọi trong
useEffecthook của chúng, nó dẫn đến một vòng lặp vô hạn.
function Parent(){
const [state, setState] = useState()
function handleChange(input){
setState(...)
}
return <Child onChange={handleChange} />
}
function Child({onChange}){
const [state, setState] = useState()
useEffect(()=>{
onChange(...)
},[onChange])
return "Child"
}
- Ví dụ 3: Khi bạn sử dụng
React Contextgiữ trạng thái và chỉ trả về các hàm thiết lập trạng thái, bạn cần người tiêu dùng củacontextđó không được render lại mỗi lần trạng thái cập nhật vì điều đó có thể ảnh hưởng đến hiệu suất.
const Context = React.createContext();
function ContextProvider({children}){
const [state, setState] = useState([]);
// Should use `useCallback`
const addToState = (input) => {
setState(prev => [...prev, input]);
}
// Should use `useCallback`
const removeFromState = (input) => {
setState(prev => prev.filter(elem => elem.id !== input.id));
}
// Should use `useCallback` with empty []
const getState = () => {
return state;
}
const contextValue= React.useMemo(
() => ({ addToState , removeFromState , getState}),
[addToState , removeFromState , getState]
);
// if we used `useCallback`, our contextValue will never change, and all the subscribers will not re-render
<Context.Provider value={contextValue}>
{children}
</Context.Provider>
}
Ví dụ 4: Nếu bạn đã đăng ký với observer, timer, sự kiện tài liệu, và cần hủy đăng ký khi component bị unmount hoặc vì bất kỳ lý do nào khác. Vì vậy chúng ta cần truy cập cùng một tham chiếu để hủy đăng ký khỏi nó.
function Component(){
// should use `useCallback`
const handler = () => {...}
useEffect(() => {
element.addEventListener(eventType, handler)
return () => element.removeEventListener(eventType, handler)
}, [eventType, element])
return ...
}
Đã xong, còn có nhiều tình huống khác bạn cũng có thể sử dụng nó, nhưng tôi hy vọng những ví dụ này đã minh họa được ý tưởng chính đằng sau useCallback. Và luôn nhớ rằng bạn không cần sử dụng nó nếu chi phí của việc render lại là không đáng kể.
