TanStack Query · Phần 16 — Hiệu năng render sâu
Vì sao v5 mặc định tracked queries, tinh chỉnh notifyOnChangeProps, dùng useQueries cho danh sách động, cơ chế structural sharing bên trong và viết structuralSharing tuỳ chỉnh, cùng cách cô lập subscription để chặn re-render thừa.
React Query đã nhanh sẵn — nhưng ở quy mô lớn (bảng nghìn dòng, dashboard chục query, stream realtime), từng lần re-render thừa cộng dồn thành giật lag. Phần 7 đã chạm select và structural sharing; phần này đào tới cơ chế bên trong: chính xác cái gì khiến một component dùng useQuery re-render, và cách cắt những lần render không cần thiết mà không phá tính đúng.
Nguyên tắc xuyên suốt: đo trước, tối ưu sau. Mở React Profiler, xác định component render thừa, rồi mới áp kỹ thuật.
1. Mô hình observer — khi nào một useQuery gây re-render?
Để tối ưu re-render, trước hết phải hiểu ai báo cho ai. Mỗi lần gọi useQuery trong một component, React Query tạo một QueryObserver gắn vào một cache entry (xác định bởi queryKey). Observer là cầu nối: nó nghe cache entry, tính ra một result object ({ data, status, fetchStatus, error, ... }), và quyết định có gọi notify để buộc component re-render hay không.
┌─────────────────────────────────────────────────────────┐
│ QueryCache (một Query duy nhất cho mỗi queryKey) │
│ state = { data, status, fetchStatus, error, dataUpdatedAt }
└───────────┬─────────────────────────────────────────────┘
│ state thay đổi (fetch xong, setQueryData, invalidate…)
▼
┌─────────────────────────────────────────────────────────┐
│ QueryObserver (một cho MỖI lần gọi useQuery) │
│ 1. tính result mới (chạy `select` nếu có) │
│ 2. structural sharing: giữ tham chiếu nhánh không đổi │
│ 3. so result mới vs cũ — CHỈ field được "track" mới tính │
└───────────┬─────────────────────────────────────────────┘
│ nếu có field theo dõi đổi → notify()
▼
┌─────────────────────────────────────────────────────────┐
│ Component → React re-render │
└─────────────────────────────────────────────────────────┘
Điểm mấu chốt: một cache entry có thể có nhiều observer (nhiều component cùng dùng một queryKey). Khi entry đổi, từng observer tự tính xem component của mình có cần render không — chúng độc lập. Đó là lý do “chia nhỏ component” (mục 5) lại mạnh đến vậy.
Mặc định v5 dùng tracked queries: observer theo dõi bạn đọc field nào từ result, và chỉ re-render khi đúng field đó đổi.
function Count() {
const { data } = useQuery(customersQuery());
// Component CHỈ đọc `data` → chỉ re-render khi `data` đổi.
// KHÔNG re-render khi `isFetching`/`fetchStatus` đổi (vì ta không đọc chúng).
return <span>{data?.length}</span>;
}
Đây là khác biệt lớn so với các thư viện ngây thơ “đổi gì cũng báo”: React Query mặc định đã thông minh về việc nào đáng re-render. Bạn thường không cần làm gì — nhưng cần hiểu để biết khi nào nó không đủ.
Cơ chế dưới nắp capo: result object của useQuery được bọc trong một Proxy. Lần đầu component render, mỗi lần bạn query.data / query.isFetching là một lần get trên Proxy — observer ghi tên field đó vào tập “tracked props”. Lần sau cache đổi, observer chỉ notify nếu một field trong tập đã track thực sự khác giá trị cũ.
Gotcha tracking: Proxy chỉ ghi nhận field bạn thật sự truy cập trong khi render. Nếu bạn spread toàn bộ result (
const all = { ...query }) hoặc destructure mọi field, bạn “chạm” hết → tập track = tất cả → mất lợi ích. Đọcquery.databên trong một callback chạy sau (vdonClick) cũng không được track (Proxy chỉ active lúc render). Chỉ đọc, lúc render, đúng field bạn dùng.
2. notifyOnChangeProps & tracked properties
Tracked queries là ngầm (tự suy ra từ field bạn đọc). Khi cần tường minh — hoặc tắt hẳn tracking để đặt cứng danh sách — dùng notifyOnChangeProps:
const { data } = useQuery({
...customersQuery(),
// Chỉ re-render khi `data` hoặc `error` đổi — bỏ qua mọi thay đổi fetchStatus.
notifyOnChangeProps: ['data', 'error'],
});
Hữu ích cho component “chỉ cần data, không quan tâm spinner”: vd một cell trong bảng lớn không cần biết query đang background-refetch. Đặt notifyOnChangeProps: ['data'] để nó bất động trừ khi data thực sự đổi.
Bảng tra notifyOnChangeProps
| Giá trị | Re-render khi… | Khi nào dùng |
|---|---|---|
| (không đặt) — mặc định | Bất kỳ field bạn đọc lúc render đổi (tracked) | Gần như luôn luôn. Để v5 tự lo. |
['data', 'error'] | Chỉ data hoặc error đổi | Component không cần biết spinner/isFetching; muốn cố định danh sách bất kể bạn đọc gì |
['data'] | Chỉ data đổi | Cell/badge “câm” — chỉ vẽ lại khi data mới về, lờ mọi trạng thái nền |
'all' | Mọi thay đổi internal (kể cả dataUpdatedAt, fetchStatus) | Hành vi cũ kiểu v3; hiếm khi cần. Dùng khi debug hoặc render dataUpdatedAt |
() => [...] (hàm) | Trả danh sách động mỗi lần observer kiểm tra | Rất hiếm; cần logic động về việc field nào quan trọng |
'all'≠ tốt hơn. Nó tăng re-render. Chỉ đặt khi bạn thật sự hiển thị một field “ồn ào” như thời điểm cập nhật. Với phần lớn app, đừng đụng vàonotifyOnChangeProps— tracked queries đã đủ.
Quan hệ với tracking: đặt notifyOnChangeProps thủ công sẽ ghi đè cơ chế tracked Proxy cho query đó. Nghĩa là bạn nhận về một mảng cứng thay vì để Proxy tự suy. Đừng đặt 'all' “cho chắc” — đó chính là tắt tối ưu mặc định.
3. select — đăng ký một lát cắt hẹp
select (Phần 7) là cách thu hẹp cái component “nghe” xuống đúng phần data cần. Observer chạy select sau khi lấy data, rồi so sánh kết quả của select (không phải data gốc) để quyết định re-render. Nhờ vậy hai component đọc cùng một query nhưng select khác nhau sẽ re-render theo nhịp khác nhau.
// Component A re-render khi SỐ LƯỢNG đổi — không quan tâm nội dung từng item
const count = useQuery({ ...customersQuery(), select: (d) => d.length });
// Component B re-render khi DANH SÁCH active đổi — không quan tâm item inactive
const active = useQuery({
...customersQuery(),
select: (d) => d.filter((c) => c.active),
});
Nếu customersQuery refetch nhưng length không đổi, A không render; nếu chỉ item inactive đổi, B không render. Một query, nhiều “view” độc lập.
Yêu cầu sống còn: tham chiếu select phải ỔN ĐỊNH
select chạy lại khi: (a) data đổi, hoặc (b) chính hàm select đổi tham chiếu. Nếu bạn viết arrow inline, mỗi lần component render là một hàm select mới → observer coi như “select đổi” → chạy lại select mỗi render (tốn CPU với phép biến đổi nặng), dù vẫn nhờ structural sharing mà thường không gây re-render thừa.
// ⚠️ select inline — hàm MỚI mỗi render → select chạy lại mỗi render
const active = useQuery({
...customersQuery(),
select: (d) => d.filter((c) => c.active), // tốn CPU nếu d to / filter nặng
});
// ✅ select ổn định — useCallback giữ một tham chiếu → chỉ chạy khi data đổi
const selectActive = useCallback(
(d: Customer[]) => d.filter((c) => c.active),
[],
);
const active = useQuery({ ...customersQuery(), select: selectActive });
Quy tắc:
selectnhẹ (lấy.length, đọc một field) → để inline thoải mái, chi phí không đáng kể.selectnặng (filter/sort/map mảng lớn, tính toán phái sinh) → bọcuseCallback(hoặc đưa ra ngoài component nếu không phụ thuộc props) để không chạy lại vô ích.
Nếu select phụ thuộc một biến (vd từ khoá lọc), đưa biến vào dependency array — nhưng nhớ: đổi dependency = select đổi tham chiếu = chạy lại. Đó là đúng và mong muốn.
const selectByKeyword = useCallback(
(d: Customer[]) => d.filter((c) => c.name.includes(keyword)),
[keyword], // đổi keyword → select chạy lại; đúng ý
);
Vì observer dùng structural sharing trên đầu ra của select, nên dù filter trả mảng mới mỗi lần, nếu nội dung giống hệt lần trước, component vẫn không re-render.
4. Structural sharing — giữ tham chiếu để bỏ qua re-render
Sau mỗi lần data mới về (refetch, setQueryData), React Query chạy replaceEqualDeep: đi sâu so sánh data mới với data cũ, giữ nguyên tham chiếu mọi nhánh không đổi.
data cũ data mới (sau refetch)
┌───────────────┐ ┌───────────────┐
│ [0] Alice ●──┼──────同一?──────┼──● [0] Alice │ giống → GIỮ tham chiếu cũ
│ [1] Bob ●──┼──────同一?──────┼──● [1] Bob │ giống → GIỮ tham chiếu cũ
│ [2] Carol ●──┼──── khác! ──────┼──○ [2] Carol* │ đổi → tham chiếu MỚI
└───────────────┘ └───────────────┘
│ │
mảng ngoài chứa [2] mới → mảng MỚI, nhưng [0],[1] vẫn === cũ
Hệ quả thực tế:
- Nếu không có gì đổi sau refetch →
data === datacũ → observer thấy y hệt → không re-render dù vừa fetch xong. - Nếu chỉ
Carolđổi → chỉ phần tử[2]nhận tham chiếu mới. Một<Row item={alice} />đượcReact.memosẽ không render lại vìalicevẫn===cũ. Chỉ<Row item={carol} />render.
Đây là lý do React.memo trên row component lại “ăn khớp” hoàn hảo với React Query: structural sharing đảm bảo các prop không đổi vẫn giữ nguyên reference.
Khi nào tắt — và rủi ro
Điều kiện để replaceEqualDeep chạy đúng: data phải JSON-serializable. Với data chứa Date, Map, Set, hay class instance, so sánh sâu mặc định có thể không chính xác (hoặc tốn kém). Khi đó:
useQuery({
queryKey: ['events'],
queryFn: fetchEvents,
// Tắt cho query này nếu data không JSON-serializable và bạn tự lo tham chiếu.
structuralSharing: false,
});
// Hoặc tuỳ chỉnh toàn cục (vd hợp nhất theo cách riêng):
new QueryClient({
defaultOptions: {
queries: {
structuralSharing: (oldData, newData) => customMerge(oldData, newData),
},
},
});
structuralSharing | Hành vi | Đánh đổi |
|---|---|---|
true (mặc định) | replaceEqualDeep giữ tham chiếu nhánh không đổi | Tốt cho data JSON; tốn chút CPU so sánh sâu data rất lớn |
false | Luôn dùng data mới nguyên xi từ queryFn | Mọi prop con đổi reference → React.memo vô dụng → re-render nhiều hơn |
(old, new) => merged | Bạn tự quyết hợp nhất | Toàn quyền cho data có Date/Map/class; phải tự đảm bảo đúng |
Đừng tắt
structuralSharingtrừ khi có lý do rõ (data không serialize được, hoặc data khổng lồ khiến so sánh sâu thành nút thắt — hiếm). Nó là một trong những tối ưu “miễn phí” giá trị nhất của thư viện.
5. Cô lập component — chia nhỏ subscription
Một sai lầm hiệu năng phổ biến nhất: gọi useQuery ở component cha to rồi truyền data xuống. Khi query refetch, cả cây con render. Thay vào đó, đẩy useQuery xuống đúng component cần data (colocation):
// ❌ Page nghe query → cả page render khi refetch
function Page() {
const { data } = useQuery(statsQuery());
return (
<>
<HeavyChart /> {/* render lại dù không dùng stats */}
<StatsBadge data={data} />
</>
);
}
// ✅ Chỉ StatsBadge nghe query → chỉ nó render khi refetch
function Page() {
return (
<>
<HeavyChart />
<StatsBadge /> {/* tự gọi useQuery(statsQuery()) bên trong */}
</>
);
}
Mỗi useQuery là một điểm subscription độc lập (mục 1: một observer riêng). Đặt subscription càng gần nơi dùng data, phạm vi re-render càng hẹp. Đây thường là tối ưu có tác động lớn nhất và rẻ nhất — không cần memo, không cần select, chỉ cần di chuyển chỗ gọi hook.
Đừng sợ gọi cùng một query ở nhiều nơi. Nhờ dedupe theo
queryKey, gọiuseQuery(statsQuery())ởStatsBadgevàStatsChartchỉ tạo một network request — nhưng hai observer độc lập, mỗi cái render riêng. Đây là chuyện tốt: bạn được cô lập re-render mà không tốn thêm fetch.
Nguyên tắc đi kèm: đừng truyền cả data xuống sâu chỉ để con cháu lôi ra một field. Mỗi tầng nhận data sẽ render khi data đổi. Hoặc để mỗi lá tự useQuery + select lát cắt của nó, hoặc dùng Context cẩn thận. Truyền nguyên data qua 5 tầng = 5 tầng cùng render mỗi lần data nhúc nhích.
6. useQueries + combine — danh sách query động
Khi số lượng query không cố định (vd fetch chi tiết cho N item được chọn), bạn không thể gọi useQuery trong vòng lặp (vi phạm rules of hooks). Dùng useQueries:
import { useQueries } from '@tanstack/react-query';
function SelectedCustomers({ ids }: { ids: string[] }) {
const { data, isPending } = useQueries({
queries: ids.map((id) => customerQuery(id)),
// combine: gộp N kết quả thành MỘT giá trị; component chỉ nghe giá trị gộp.
combine: combineCustomers,
});
if (isPending) return <Spinner />;
return <List items={data} />;
}
// ✅ combine định nghĩa NGOÀI render → tham chiếu ổn định
function combineCustomers(results: UseQueryResult<Customer>[]) {
return {
data: results.map((r) => r.data).filter((c): c is Customer => Boolean(c)),
isPending: results.some((r) => r.isPending),
};
}
combine quan trọng cho hiệu năng: thay vì component nghe N kết quả riêng lẻ (re-render khi bất kỳ cái nào đổi), nó nghe một giá trị gộp, và chỉ render khi giá trị gộp thực sự khác (structural sharing áp lên đầu ra của combine).
Caveat tham chiếu ổn định: giống
select, nếu bạn viếtcombineinline thì mỗi render là một hàm mới →combinechạy lại mỗi render. Với N nhỏ thì không sao; với N lớn hoặc phép gộp nặng, đưacombinera ngoài (như trên) hoặcuseCallback. React Query vẫn structural-share đầu ra nên thường không gây re-render thừa — nhưng bạn tiết kiệm CPU bằng cách không chạy lại phép gộp vô ích.
combine cũng là nơi lý tưởng để tính giá trị phái sinh (tổng, trung bình, gộp lỗi) một lần, thay vì rải .map/.reduce khắp JSX con.
7. Danh sách lớn — memo, keys, virtualization
Bảng/list dài là nơi re-render thừa “ăn” nhiều FPS nhất. Bốn đòn bẩy, theo thứ tự:
- Đừng truyền cả
dataxuống từng row. Truyền đúngitemcủa row đó. Nhờ structural sharing (mục 4), row không đổi giữ nguyên reference. React.memorow component với prop là chínhitem. Khi list refetch nhưng chỉ vài item đổi, chỉ những row đó render.keyổn định, định danh thật (id của bản ghi), không dùng index.keyindex khiến React tái dùng nhầm DOM khi list sắp xếp/lọc → row “nhảy” và render sai.- Virtualization khi list cực dài (hàng nghìn dòng): chỉ mount các row đang trong viewport. Dùng
@tanstack/react-virtual. React Query lo data, virtual lo số DOM node.
// Row được memo: chỉ render khi `item` của NÓ đổi reference (structural sharing lo việc đó)
const Row = React.memo(function Row({ item }: { item: Customer }) {
return <li>{item.name} — {item.email}</li>;
});
function CustomerList() {
const { data = [] } = useQuery(customersQuery());
return (
<ul>
{data.map((c) => (
<Row key={c.id} item={c} /> // key = id thật, KHÔNG phải index
))}
</ul>
);
}
Ghép đôi quan trọng:
React.memorow chỉ phát huy khi prop giữ reference giữa các lần render — và đó chính là điều structural sharing của React Query đảm bảo. Nếu bạnselect: (d) => d.map(transform)tạo object mới cho mọi row mỗi lần, bạn vô tình phá reference →React.memovô dụng. Hoặc memo phép transform, hoặc transform trong chính row.
8. React 18/19 & React Compiler
Bối cảnh React hiện đại làm nhẹ gánh tối ưu thủ công — nhưng không thay thế các nguyên tắc trên:
- Automatic batching (React 18+): nhiều
setState/notify trong cùng một tick (kể cả trong promise,setTimeout, event handler) được gộp thành một lần render. Khi 3 query cùng resolve gần nhau, bạn không còn ăn 3 lần render liên tiếp như React 17. React Query tận dụng điều này sẵn — bạn không phải làm gì. useSuspenseQuery+ streaming (React 18/19): kết hợp Suspense, data về theo dòng, tránh “loading nhấp nháy” toàn trang (đã bàn ở Phần 7 & 10).- React Compiler (React 19): tự chèn memo hoá ở mức biên dịch — nhiều
useMemo/useCallback/React.memothủ công trở nên thừa. Nhưngselect/combinecủa React Query không nằm trong phạm vi Compiler tối ưu hoàn toàn (chúng là option truyền vào hook), nên tham chiếu ổn định choselect/combinevẫn nên cân nhắc khi phép biến đổi nặng. Khi bật Compiler, hãy đo lại trước khi gỡ memo cũ.
Định hướng: Coi React Compiler là “memo hoá mặc định cho component”, còn
select/combinereference vẫn là quyết định của bạn. Đừng vội xoáuseCallbackquanhselectnặng chỉ vì đã bật Compiler — hãy đo bằng Profiler.
9. Thứ tự tối ưu (nhắc lại & mở rộng)
Từ Phần 7, theo thứ tự tác động giảm dần — luôn làm từ trên xuống:
- Bundle — code splitting, lazy route. Không tải JS thừa.
- Data — prefetch, tránh waterfall (Phần 11). Data tới sớm.
- Kiến trúc subscription — đặt
useQuerygần nơi dùng (mục 5), đừng truyền cảdataxuống sâu (mục 7). - Thu hẹp cái component nghe —
selectlát cắt hẹp (mục 3) →notifyOnChangeProps(mục 2) →useQueries.combine(mục 6). - Memo cấp render —
React.memorow + key ổn định (mục 7), virtualization nếu list khổng lồ.
Đừng nhảy vào micro-optimize
selectkhi component cha to vẫn nghe query và kéo cả cây render. Sửa kiến trúc subscription (bước 3) trước — nó thường xoá phần lớn re-render thừa chỉ bằng việc dời chỗ gọi hook.
Và trên hết: đo trước, tối ưu sau. Mở React DevTools Profiler, ghi một lần refetch, tìm component “render mà không đổi gì”, rồi mới áp đúng kỹ thuật cho đúng chỗ.
10. Gotchas thường gặp
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
| Component render mỗi lần background-refetch dù data y hệt | Đọc isFetching/fetchStatus lúc render (tracked) hoặc spread cả result | Chỉ đọc data; hoặc notifyOnChangeProps: ['data'] |
select chạy lại mỗi render, tốn CPU | select viết inline → hàm mới mỗi render | useCallback (hoặc đưa ra ngoài component) cho select nặng |
React.memo row vô dụng, vẫn render hết | select/transform tạo object mới cho mọi item; hoặc truyền cả data xuống | Giữ reference (structural sharing); memo transform; truyền đúng item |
| Mất hết lợi ích tracked queries | const all = { ...query } hoặc destructure mọi field | Chỉ destructure field thật dùng, lúc render |
useQueries render mỗi khi 1 query con đổi dù kết quả gộp như cũ | combine viết inline → reference đổi mỗi render | Đưa combine ra ngoài / useCallback |
| Spinner “nhấp nháy” liên tục trên list | Hiển thị theo isFetching (đổi mỗi background-refetch) | Dùng isPending/status cho lần load đầu; tách indicator nền ra component riêng |
| List sắp xếp/lọc làm row “nhảy” và render sai | key={index} | key={item.id} — id định danh thật |
Đặt notifyOnChangeProps: 'all' “cho chắc” | Hiểu nhầm 'all' an toàn hơn | Bỏ đi — 'all' tăng re-render; để mặc định tracked |
11. Recipes thực chiến
Hook select hẹp, tái dùng (tham chiếu ổn định):
// Tách select ra hook → mọi nơi dùng đều có select ổn định, type rõ.
function useActiveCustomers() {
return useQuery({
...customersQuery(),
select: selectActive, // hằng module-level → reference ổn định
});
}
function selectActive(d: Customer[]): Customer[] {
return d.filter((c) => c.active);
}
Row được cô lập + memo (ăn khớp structural sharing):
const CustomerRow = React.memo(function CustomerRow({ id }: { id: string }) {
// Mỗi row tự đăng ký lát cắt của riêng nó → chỉ row có item đổi mới render.
const { data } = useQuery({
...customersQuery(),
select: useCallback((d: Customer[]) => d.find((c) => c.id === id), [id]),
});
if (!data) return null;
return <li>{data.name}</li>;
});
combine cho giá trị phái sinh (tính một lần):
function useOrderTotals(ids: string[]) {
return useQueries({
queries: ids.map((id) => orderQuery(id)),
combine: combineTotals, // ngoài render → ổn định
});
}
function combineTotals(results: UseQueryResult<Order>[]) {
const orders = results.map((r) => r.data).filter((o): o is Order => Boolean(o));
return {
total: orders.reduce((sum, o) => sum + o.amount, 0),
isPending: results.some((r) => r.isPending),
};
}
Tách indicator nền để hết nhấp nháy:
function List() {
const { data } = useQuery({ ...itemsQuery(), notifyOnChangeProps: ['data'] });
return <ul>{data?.map((i) => <Row key={i.id} item={i} />)}</ul>;
}
// Indicator nền sống ở component RIÊNG → chỉ NÓ render khi isFetching đổi.
function BackgroundSpinner() {
const isFetching = useIsFetching({ queryKey: itemsQuery().queryKey });
return isFetching ? <span className="dot" /> : null;
}
12. Bài tập
1. “Tracked queries” của v5 nghĩa là gì, và làm sao bạn vô tình phá nó?
Lời giải
Tracked queries theo dõi field nào bạn đọc từ kết quả useQuery (qua Proxy) và chỉ re-render khi đúng field đó đổi. Bạn phá nó khi “chạm” hết field — vd spread toàn bộ kết quả ({ ...query }) hoặc destructure mọi field — khiến observer phải báo trên mọi thay đổi. Chỉ đọc field thật sự dùng để giữ lợi ích.
2. Khi nào cần useQueries thay vì nhiều useQuery, và combine giúp gì cho hiệu năng?
Lời giải
Dùng useQueries khi số lượng query động/không cố định (không thể gọi useQuery trong vòng lặp). combine gộp N kết quả thành một giá trị duy nhất; component nghe giá trị gộp thay vì N kết quả riêng, nên chỉ re-render khi giá trị gộp thực sự đổi — giảm re-render khi một query con thay đổi không ảnh hưởng kết quả gộp.
3. Vì sao “đặt useQuery gần nơi dùng data” thường là tối ưu re-render tác động lớn nhất?
Lời giải
Mỗi useQuery là một điểm subscription; component chứa nó (và cây con) re-render khi query đổi. Gọi ở cha to rồi truyền prop khiến cả cây con render dù phần lớn không dùng data. Đẩy subscription xuống đúng component cần thu hẹp phạm vi re-render xuống tối thiểu — rẻ và hiệu quả hơn nhiều so với micro-optimize select.
4. Hai component cùng gọi useQuery(customersQuery()) — một dùng select: (d) => d.length, một dùng select: (d) => d.filter((c) => c.active). Sau một refetch chỉ thêm một customer inactive, component nào re-render? Vì sao?
Lời giải
Observer so sánh đầu ra của select, không phải data gốc. Thêm một item inactive: length đổi (tăng 1) → component đếm re-render; còn danh sách active (sau structural sharing) có nội dung y hệt → đầu ra select giữ reference → component active không re-render. Một query, hai nhịp render độc lập — đó là sức mạnh của select.
5. Bạn viết select: (d) => d.filter((c) => c.active) inline và combine inline trong useQueries. Cả hai “chạy lại mỗi render”. Điều này có gây re-render thừa không? Chi phí thực là gì, và khi nào phải sửa?
Lời giải
Thường không gây re-render thừa: React Query áp structural sharing lên đầu ra của select/combine, nên dù hàm chạy lại và trả mảng mới, nếu nội dung giống thì component không render. Chi phí thực là CPU: phép filter/gộp chạy lại mỗi render dù data không đổi. Phải sửa (đưa hàm ra ngoài / useCallback) khi phép biến đổi nặng hoặc data lớn — lúc đó CPU lãng phí mới đáng kể.
Nâng cao: Lấy một trang gọi useQuery ở cha và truyền prop xuống. Mở React Profiler, ghi lại một lần refetch. Đẩy useQuery xuống component lá, đo lại — so sánh số component re-render trước/sau. Sau đó thêm React.memo cho row + key={item.id} và ghi lại lần ba: xác nhận chỉ row có item đổi mới render.
Tóm tắt
- Mô hình observer: mỗi
useQuery= một observer gắn vào một cache entry; một entry có thể nuôi nhiều observer độc lập. Observer tính result, structural-share, rồi mới quyết địnhnotify. - v5 mặc định tracked queries (Proxy): chỉ re-render khi field bạn đọc lúc render đổi. Đừng spread toàn bộ result (
{ ...query }) — phá tracking. notifyOnChangePropsđặt cứng danh sách field (['data']cho component “câm”);'all'tăng re-render — hiếm khi cần.selectđăng ký một lát cắt hẹp → nhiều view độc lập trên cùng query.selectnặng phải có tham chiếu ổn định (useCallback/hằng module) kẻo chạy lại mỗi render.- Structural sharing (
replaceEqualDeep) giữ tham chiếu nhánh không đổi →React.memorow hoạt động. Chỉ tắt khi data không JSON-serializable hoặc khổng lồ. - Cô lập subscription (đặt
useQuerygần nơi dùng, đừng truyền cảdataxuống sâu) thường là tối ưu re-render lớn nhất. useQueries+combinecho danh sách query động; giữcombineổn định. Lists:React.memo+key={id}thật + virtualization nếu khổng lồ.- React 18 batching & React 19 Compiler giảm memo thủ công, nhưng
select/combinereference vẫn do bạn — đo trước, tối ưu sau. Thứ tự: bundle → data → kiến trúc subscription → thu hẹp cái nghe → memo render.
Phần tiếp theo
Phần 17 — Type-safety đỉnh cao: typing toàn trình từ queryFn tới component, generic query factory tái dùng, dùng skipToken thay cho enabled để type hẹp đúng, suy luận type qua DataTag/queryOptions, kết hợp zod infer, và loại bỏ as khỏi lớp data.