Chào mừng các bạn quay trở lại với chuỗi bài viết React Roadmap! Trên hành trình xây dựng các ứng dụng React mạnh mẽ và hiệu quả, chúng ta đã cùng nhau tìm hiểu về những khái niệm cốt lõi như React là gì, sự khác biệt giữa Class và Functional Components, hiểu về JSX, cách quản lý dữ liệu với Props và State, xử lý sự kiện, làm việc với danh sách, và mới đây là sức mạnh của useState và useEffect.
Khi ứng dụng của chúng ta ngày càng phức tạp, việc đảm bảo hiệu năng trở nên cực kỳ quan trọng. Đôi khi, các thao tác tính toán nặng, việc tạo lại các hàm không cần thiết, hoặc nhu cầu truy cập các giá trị bền vững qua các lần render mà không gây re-render có thể làm chậm ứng dụng của bạn. Đây chính là lúc bộ ba quyền năng: useCallback
, useMemo
, và useRef
phát huy tác dụng. Chúng là những công cụ tối ưu hóa và quản lý trạng thái nâng cao, giúp bạn viết code React hiệu quả và có thể mở rộng hơn.
Trong bài viết này, chúng ta sẽ cùng đi sâu vào từng Hook này, tìm hiểu *khi nào* và *vì sao* nên sử dụng chúng, cùng với những ví dụ thực tế để giúp bạn nắm vững cách áp dụng chúng trong các dự án của mình. Hãy bắt đầu!
Mục lục
Vấn đề: Khi Functional Component Re-render
Trước khi tìm hiểu các Hook này giải quyết vấn đề gì, chúng ta cần hiểu cách Functional Component hoạt động. Khác với Class Component, Functional Component về cơ bản là một hàm JavaScript. Mỗi khi state hoặc props của một component (hoặc component cha của nó) thay đổi, React sẽ gọi lại hàm component đó để xác định UI mới cần hiển thị.
Quá trình này, gọi là “re-render”, là cách React cập nhật giao diện. Thông thường, nó rất nhanh. Tuy nhiên, trong một số trường hợp, re-render có thể trở thành nút thắt cổ chai:
- Thực hiện các phép tính phức tạp: Nếu mỗi lần component re-render, bạn lại phải thực hiện một phép tính tốn thời gian (ví dụ: lọc, sắp xếp một mảng lớn, tính toán đồ họa phức tạp), ứng dụng có thể bị giật lag.
- Truyền Prop là hàm hoặc object mới: Khi bạn truyền một hàm hoặc một object (không phải kiểu nguyên thủy như chuỗi, số, boolean) từ component cha xuống component con, mỗi lần component cha re-render, hàm hoặc object đó sẽ được tạo lại với một địa chỉ bộ nhớ mới. Ngay cả khi nội dung của hàm/object không đổi, React vẫn coi đó là một prop mới.
- Gây re-render không mong muốn cho component con: Nếu component con được tối ưu hóa bằng
React.memo
(một API bọc component để chỉ re-render khi props thay đổi), việc truyền prop là hàm hoặc object mới trên mỗi lần render của cha sẽ khiến con luôn re-render, làm mất tác dụng củaReact.memo
. - Kích hoạt lại
useEffect
không cần thiết: Tương tự, nếu bạn sử dụng một hàm hoặc object làm dependency trong mảng phụ thuộc củauseEffect
, việc chúng được tạo lại trên mỗi render sẽ khiếnuseEffect
chạy lại ngay cả khi logic bên trong hàm/object đó không thay đổi.
useCallback
và useMemo
ra đời để giải quyết những vấn đề liên quan đến việc tạo lại hàm và tính toán giá trị không cần thiết trong quá trình re-render, trong khi useRef
giúp chúng ta lưu trữ các giá trị *bền vững* qua các lần render mà không gây ra hiệu ứng re-render.
useCallback: Ghi nhớ (Memoizing) Hàm
useCallback là gì?
useCallback
là một Hook của React giúp bạn “ghi nhớ” (memoize) một hàm callback. Điều này có nghĩa là React sẽ trả về cùng một tham chiếu (địa chỉ bộ nhớ) của hàm giữa các lần render, trừ khi các giá trị trong mảng phụ thuộc (dependency array) của nó thay đổi.
Vì sao nên dùng useCallback?
Lý do chính để sử dụng useCallback
là để duy trì tính đồng nhất của tham chiếu hàm qua các lần render. Điều này cực kỳ hữu ích trong hai trường hợp chính:
- Khi truyền hàm làm prop cho component con được tối ưu hóa với
React.memo
: Như đã đề cập ở trên, nếu component con được bọc bởiReact.memo
, nó sẽ chỉ re-render khi props của nó thay đổi. Nếu một trong những props đó là một hàm và hàm này được tạo mới trên mỗi lần render của component cha (vì không dùnguseCallback
), component con sẽ luôn coi đó là một prop mới và re-render, phá vỡ cơ chế tối ưu hóa củaReact.memo
. Sử dụnguseCallback
đảm bảo rằng component con chỉ re-render khi các dependency thực sự thay đổi (dẫn đến việc tạo lại hàm). - Khi sử dụng hàm làm dependency cho
useEffect
hoặcuseMemo
: Đôi khi bạn cần sử dụng một hàm được định nghĩa bên trong component làm dependency chouseEffect
hoặcuseMemo
. Nếu hàm đó được tạo mới trên mỗi render,useEffect
hoặcuseMemo
sẽ chạy lại không cần thiết. Bọc hàm bằnguseCallback
giúp React chỉ chạy lại hiệu ứng hoặc tính toán lại giá trị khi các dependency *của hàm callback* thay đổi.
Nói cách khác, useCallback
giúp ngăn chặn việc tạo lại các hàm không cần thiết, từ đó cải thiện hiệu suất, đặc biệt khi kết hợp với React.memo
hoặc quản lý dependencies của các Hook khác.
Cú pháp của useCallback
const memoizedCallback = useCallback(
() => {
// Do something...
},
[dependencies] // Mảng các giá trị mà hàm callback phụ thuộc vào
);
- Tham số đầu tiên là hàm callback mà bạn muốn ghi nhớ.
- Tham số thứ hai là một mảng các dependency. Nếu bất kỳ giá trị nào trong mảng này thay đổi giữa các lần render, hàm callback sẽ được tạo lại. Nếu mảng rỗng
[]
, hàm sẽ chỉ được tạo ra một lần duy nhất trong suốt vòng đời của component.
Ví dụ về useCallback
Hãy xem xét một ví dụ đơn giản. Giả sử chúng ta có một component Parent
truyền một hàm xử lý click xuống component con Button
được tối ưu hóa bằng React.memo
. (Bạn có thể xem lại bài viết về Xử Lý Sự Kiện và Kết hợp Component để hiểu rõ hơn về cách truyền props và component con).
Component con (Button.js):
import React from 'react';
// Sử dụng React.memo để tối ưu
const Button = React.memo(({ handleClick, children }) => {
console.log('Button component re-rendered:', children);
return <button onClick={handleClick}>{children}</button>;
});
export default Button;
Component cha (Parent.js) – KHÔNG dùng useCallback:
import React, { useState } from 'react';
import Button from './Button';
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('React');
// Hàm này được tạo lại mỗi khi Parent re-render
const handleButtonClick = () => {
setCount(count + 1);
console.log('Button clicked!');
};
console.log('Parent component re-rendered');
return (
<div>
<h1>Count: {count}</h1>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
{/* Truyền hàm handleButtonClick xuống component con */}
<Button handleClick={handleButtonClick}>Click me</Button>
{/* Button khác không dùng hàm memoized */}
<Button handleClick={() => console.log('Another button clicked')}>Another Button</Button>
</div>
);
}
export default Parent;
Trong ví dụ trên, khi bạn gõ vào input (thay đổi state name
), component Parent
sẽ re-render. Vì handleButtonClick
được định nghĩa lại mỗi lần hàm Parent
được gọi, React sẽ coi prop handleClick
của component Button
đã thay đổi. Mặc dù Button
được bọc bởi React.memo
, nó vẫn re-render.
Component cha (Parent.js) – Dùng useCallback:
import React, { useState, useCallback } from 'react';
import Button from './Button';
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('React');
// Hàm này chỉ được tạo lại khi 'count' thay đổi
const handleButtonClick = useCallback(() => {
setCount(count + 1); // Lưu ý: 'count' là dependency ngầm ở đây
console.log('Button clicked!');
}, [count]); // count là dependency
console.log('Parent component re-rendered');
return (
<div>
<h1>Count: {count}</h1>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
{/* Truyền hàm handleButtonClick đã được ghi nhớ */}
<Button handleClick={handleButtonClick}>Click me</Button>
{/* Button khác không dùng hàm memoized */}
<Button handleClick={() => console.log('Another button clicked')}>Another Button</Button>
</div>
);
}
export default Parent;
Với phiên bản sử dụng useCallback
, khi bạn gõ vào input (chỉ thay đổi name
), component Parent
vẫn re-render, nhưng hàm handleButtonClick
không được tạo lại vì dependency count
không thay đổi. Do đó, prop handleClick
truyền xuống component Button
vẫn giữ nguyên tham chiếu, và Button
không re-render nhờ React.memo
.
Khi nào KHÔNG nên dùng useCallback?
Đừng lạm dụng useCallback
! Nó cũng có chi phí overhead nhất định (React cần lưu trữ hàm và mảng dependencies). Chỉ sử dụng khi:
- Bạn đang truyền hàm xuống component con được tối ưu hóa bằng
React.memo
. - Hàm đó là dependency của
useEffect
hoặcuseMemo
và việc tạo lại hàm ảnh hưởng đến logic hoặc hiệu suất của các Hook đó. - Hàm thực hiện một thao tác rất nặng (ít phổ biến).
Trong các trường hợp đơn giản, việc tạo lại hàm là rất nhanh và chi phí của useCallback
có thể lớn hơn lợi ích mang lại.
useMemo: Ghi nhớ (Memoizing) Giá trị
useMemo là gì?
Tương tự như useCallback
ghi nhớ hàm, useMemo
là một Hook của React giúp bạn “ghi nhớ” (memoize) một giá trị được tính toán. Điều này có nghĩa là React sẽ chỉ tính toán lại giá trị đó khi các giá trị trong mảng phụ thuộc (dependency array) của nó thay đổi.
Vì sao nên dùng useMemo?
Mục đích chính của useMemo
là tránh các phép tính lại tốn kém trên mỗi lần render:
- Tính toán các giá trị phức tạp: Nếu bạn có một phép tính (ví dụ: xử lý một mảng dữ liệu lớn, tính toán phức tạp) mà kết quả của nó chỉ phụ thuộc vào một số state hoặc prop nhất định, bạn có thể bọc phép tính đó trong
useMemo
. React sẽ chỉ chạy lại phép tính khi các dependency thay đổi, thay vì chạy mỗi lần component re-render. - Tạo object hoặc array làm prop cho component con được tối ưu hóa với
React.memo
: Giống như hàm, object và array cũng là kiểu tham chiếu trong JavaScript. Nếu bạn tạo một object hoặc array mới trên mỗi render để truyền xuống component con được tối ưu hóa, component con sẽ luôn re-render.useMemo
giúp bạn ghi nhớ object/array đó để nó chỉ được tạo lại khi các dependency thay đổi. - Làm dependency cho
useEffect
hoặcuseCallback
: Tương tự việc ghi nhớ hàm cho dependencies, bạn có thể ghi nhớ các giá trị phức tạp được dùng làm dependency để tránh chạy lại Hook không cần thiết.
useMemo
giúp cải thiện hiệu suất bằng cách giảm bớt khối lượng công việc tính toán trong quá trình re-render.
Cú pháp của useMemo
const memoizedValue = useMemo(
() => {
// Perform expensive calculation
return value;
},
[dependencies] // Mảng các giá trị mà phép tính phụ thuộc vào
);
- Tham số đầu tiên là một “factory function” (hàm tạo) không có tham số. React sẽ gọi hàm này để tính toán giá trị cần ghi nhớ.
- Tham số thứ hai là một mảng các dependency. Nếu bất kỳ giá trị nào trong mảng này thay đổi, factory function sẽ được gọi lại để tính toán giá trị mới. Nếu mảng rỗng
[]
, giá trị sẽ chỉ được tính toán một lần duy nhất. useMemo
trả về giá trị đã được ghi nhớ.
Ví dụ về useMemo
Hãy xem xét một ví dụ về việc tính toán phức tạp.
import React, { useState, useMemo } from 'react';
function Calculator() {
const [number, setNumber] = useState(0);
const [multiplier, setMultiplier] = useState(1);
// Một phép tính tốn kém
const calculateExpensiveValue = (num, mul) => {
console.log('Performing expensive calculation...');
// Giả lập độ trễ tính toán
let i = 0;
while (i < 1000000000) i++;
return num * mul;
};
// Sử dụng useMemo để ghi nhớ kết quả tính toán
// Kết quả chỉ được tính lại khi number hoặc multiplier thay đổi
const expensiveValue = useMemo(() => {
return calculateExpensiveValue(number, multiplier);
}, [number, multiplier]); // Dependencies: number và multiplier
console.log('Calculator component re-rendered');
return (
<div>
<h1>Expensive Calculation</h1>
<input
type="number"
value={number}
onChange={(e) => setNumber(parseInt(e.target.value))}
/>
<input
type="number"
value={multiplier}
onChange={(e) => setMultiplier(parseInt(e.target.value))}
/>
<p>Result: {expensiveValue}</p>
<p>Multiplier: {multiplier}</p> {/* Hiển thị multiplier để thấy re-render */}
</div>
);
}
export default Calculator;
Trong ví dụ này, phép tính calculateExpensiveValue
được bọc trong useMemo
. Nó chỉ chạy lại khi number
hoặc multiplier
thay đổi. Nếu bạn có thêm state khác trong component Calculator
(ví dụ: một input text cho tên người dùng) và thay đổi state đó, component vẫn re-render, nhưng dòng log “Performing expensive calculation…” sẽ không xuất hiện, chứng tỏ phép tính tốn kém đã không chạy lại nhờ useMemo
. Tuy nhiên, nếu bạn thay đổi number
hoặc multiplier
, phép tính sẽ được thực hiện lại.
Khi nào KHÔNG nên dùng useMemo?
Giống như useCallback
, đừng lạm dụng useMemo
. Chỉ dùng khi:
- Bạn đang thực hiện một phép tính thực sự tốn kém và việc chạy lại nó trên mỗi render gây ra vấn đề hiệu năng.
- Bạn tạo một object hoặc array mới làm prop cho component con được tối ưu hóa bằng
React.memo
, và việc tạo mới này gây re-render không cần thiết cho component con. - Giá trị tính toán được sử dụng làm dependency cho
useEffect
hoặcuseCallback
và bạn muốn kiểm soát khi nào các Hook đó chạy lại.
Đối với các phép tính đơn giản hoặc khi giá trị không được truyền xuống component con được tối ưu hóa, chi phí của useMemo
có thể không đáng kể so với lợi ích.
useRef: Lưu trữ Giá trị có thể thay đổi qua các lần Render
useRef là gì?
useRef
là một Hook của React trả về một object “ref” có thể thay đổi được. Object này có một thuộc tính duy nhất là .current
, được khởi tạo bằng giá trị bạn truyền vào làm đối số.
Điều quan trọng nhất về useRef
là:
- Giá trị trong
.current
được duy trì (persist) qua toàn bộ vòng đời của component, bất chấp các lần re-render. - Thay đổi giá trị của
.current
KHÔNG kích hoạt re-render component. Đây là điểm khác biệt cốt lõi so vớiuseState
.
Bạn có thể xem lại bài viết chi tiết về Refs trong React để hiểu thêm về cách tiếp cận DOM trực tiếp, đó là một trong những trường hợp sử dụng phổ biến nhất của useRef
.
Vì sao nên dùng useRef?
useRef
có nhiều trường hợp sử dụng quan trọng:
- Truy cập các phần tử DOM: Đây là mục đích ban đầu của “refs”. Bạn có thể gắn một ref vào một phần tử JSX và sau đó truy cập phần tử DOM đó thông qua
ref.current
(ví dụ: focus vào input, đo kích thước phần tử). - Lưu trữ các giá trị có thể thay đổi mà không gây re-render: Đôi khi bạn cần lưu trữ một giá trị thay đổi theo thời gian (ví dụ: ID của một timer interval, giá trị trước đó của một state, một flag boolean) nhưng việc thay đổi giá trị này không cần thiết phải cập nhật UI. Sử dụng
useState
sẽ gây re-render mỗi khi giá trị thay đổi, trong khiuseRef
cho phép bạn lưu trữ và cập nhật giá trị này mà không ảnh hưởng đến render cycle. - Lưu trữ các giá trị cần truy cập trong các hàm callback/effect mà không thêm vào dependency array: Đôi khi bạn có một giá trị (ví dụ: state hiện tại) cần truy cập bên trong một hàm callback (được tạo bằng
useCallback
) hoặc một hiệu ứng (useEffect
) nhưng bạn không muốn giá trị đó là dependency, vì nó sẽ khiến callback/effect chạy lại quá thường xuyên. Bạn có thể lưu giá trị đó vào một ref và truy cập thông quaref.current
bên trong callback/effect. *Lưu ý: Sử dụng cách này cần cẩn trọng để tránh các bug liên quan đến stale closures.*
useRef
cung cấp một cách để “thoát ly” khỏi luồng dữ liệu (data flow) thông thường của React khi bạn cần lưu trữ thông tin bền vững mà không muốn nó ảnh hưởng đến việc re-render.
Cú pháp của useRef
const refContainer = useRef(initialValue);
- Tham số đầu tiên là giá trị khởi tạo cho thuộc tính
.current
. useRef
trả về một object ref mới mỗi lần component được mount (gắn vào DOM). Object ref này sẽ được giữ nguyên giữa các lần re-render.- Bạn có thể truy cập hoặc thay đổi giá trị thông qua
refContainer.current
.
Ví dụ về useRef
Ví dụ 1: Truy cập DOM
import React, { useRef, useEffect } from 'react';
function InputFocus() {
const inputRef = useRef(null);
useEffect(() => {
// Truy cập phần tử DOM thông qua inputRef.current
// và gọi phương thức focus()
if (inputRef.current) {
inputRef.current.focus();
}
}, []); // Mảng rỗng đảm bảo effect chỉ chạy một lần sau lần render đầu tiên
return (
<div>
<label>Enter your name:</label>
<input type="text" ref={inputRef} /> {/* Gắn ref vào input */}
</div>
);
}
export default InputFocus;
Ví dụ này sử dụng useRef
để lấy tham chiếu đến phần tử input và tự động focus vào nó khi component được render lần đầu tiên.
Ví dụ 2: Lưu trữ giá trị không gây re-render (ví dụ: ID timer)
import React, { useState, useRef, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
// useRef để lưu trữ ID của interval
// Thay đổi timerIdRef.current không gây re-render
const timerIdRef = useRef(null);
useEffect(() => {
// Khởi tạo interval và lưu ID vào ref
timerIdRef.current = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// Cleanup function: xóa interval khi component unmount
return () => {
clearInterval(timerIdRef.current);
};
}, []); // Mảng rỗng đảm bảo effect chỉ chạy một lần khi mount/unmount
const stopTimer = () => {
// Truy cập ID từ ref để xóa interval
clearInterval(timerIdRef.current);
};
return (
<div>
<h1>Timer: {count}</h1>
<button onClick={stopTimer}>Stop Timer</button>
</div>
);
}
export default Timer;
Trong ví dụ này, chúng ta dùng useRef
để lưu ID của interval timer. Khi setCount
được gọi, component re-render, nhưng giá trị timerIdRef.current
vẫn được giữ nguyên, cho phép hàm stopTimer
sau này truy cập đúng ID để xóa interval. Nếu dùng useState
để lưu ID timer, mỗi lần set state, component lại re-render không cần thiết.
Khi nào KHÔNG nên dùng useRef?
- Không lưu trữ các giá trị mà việc thay đổi của chúng cần cập nhật giao diện người dùng. Đó là nhiệm vụ của state (
useState
hoặcuseReducer
). - Không nên đọc hoặc ghi
ref.current
trong phần thân của functional component trong quá trình render (trừ khi là giá trị khởi tạo hoặc đọc trong các hàm xử lý sự kiện, effects). Việc này có thể gây ra side effects hoặc Race Condition vì ref có thể thay đổi bất cứ lúc nào.
Tóm tắt: So sánh useCallback, useMemo, và useRef
Để dễ hình dung sự khác biệt giữa ba Hook này, hãy xem bảng tóm tắt sau:
Hook | Mục đích chính | Ghi nhớ/Lưu trữ gì? | Khi nào giá trị/hàm được cập nhật? | Thay đổi có gây Re-render không? | Trường hợp sử dụng phổ biến |
---|---|---|---|---|---|
useCallback |
Tối ưu hiệu năng bằng cách ghi nhớ hàm | Tham chiếu hàm | Khi các giá trị trong mảng dependency thay đổi | Không trực tiếp gây re-render component hiện tại (nhưng hàm mới có thể khiến component con được React.memo bọc re-render) |
Truyền hàm làm prop cho component con được React.memo bọc; làm dependency cho useEffect /useMemo . |
useMemo |
Tối ưu hiệu năng bằng cách ghi nhớ giá trị | Giá trị được tính toán (số, chuỗi, object, array, …) | Khi các giá trị trong mảng dependency thay đổi | Không trực tiếp gây re-render component hiện tại (nhưng giá trị mới có thể khiến component con được React.memo bọc re-render) |
Tính toán giá trị phức tạp; tạo object/array làm prop cho component con được React.memo bọc; làm dependency cho useEffect /useCallback . |
useRef |
Lưu trữ giá trị bền vững qua các lần render | Một object { current: value } có thể thay đổi được |
Khi bạn gán giá trị mới cho .current |
Không | Truy cập DOM trực tiếp; lưu trữ ID timer, giá trị trước đó của state, các giá trị cần bền vững nhưng không cần re-render UI. |
Bảng này giúp bạn nhanh chóng phân biệt chức năng và mục đích sử dụng của từng Hook. useCallback
và useMemo
thiên về tối ưu hiệu năng thông qua “ghi nhớ” để tránh tính toán/tạo lại không cần thiết, trong khi useRef
thiên về lưu trữ “trạng thái” bền vững qua các lần render mà không kích hoạt UI update.
Kết luận
useCallback
, useMemo
, và useRef
là những công cụ mạnh mẽ trong bộ Hook của React, giúp chúng ta kiểm soát chặt chẽ hơn hành vi của functional components, đặc biệt là về hiệu năng và quản lý các giá trị bền vững.
Tuy nhiên, điều quan trọng cần nhớ là không phải lúc nào cũng cần sử dụng chúng. Áp dụng bừa bãi các Hook này có thể làm code khó đọc hơn và thậm chí gây ra overhead không cần thiết. Hãy luôn bắt đầu với code đơn giản, chỉ tối ưu khi bạn nhận thấy có vấn đề về hiệu năng (thường thông qua việc sử dụng React DevTools Profiler). Hiểu rõ “khi nào và vì sao” là chìa khóa để sử dụng chúng một cách hiệu quả.
Trên React Roadmap, việc nắm vững các Hook cơ bản như useState và useEffect, cùng với các Hook nâng cao hơn như useCallback
, useMemo
, và useRef
, sẽ trang bị cho bạn những kỹ năng cần thiết để xây dựng các ứng dụng React phức tạp và có hiệu suất cao. Chúng ta cũng đã tìm hiểu về cách tạo Custom Hooks để tái sử dụng logic, nơi mà các Hook cơ bản này chính là nền tảng.
Hy vọng bài viết này đã giúp bạn có cái nhìn rõ ràng hơn về vai trò và cách sử dụng của bộ ba Hook quan trọng này. Hãy thực hành và thử nghiệm chúng trong các dự án của bạn để cảm nhận sự khác biệt!
Hẹn gặp lại các bạn trong những bài viết tiếp theo của chuỗi React Roadmap!