Bạn dùng useMemo, useCallback, useRef mỗi ngày — nhưng thử trả lời: vì sao hook không được đặt trong if? Vì sao deps array so sánh được giữa hai lần render? Vì sao object của useRef không bao giờ bị tạo lại? Docs chỉ cho bạn LUẬT; cách nhớ luật lâu nhất là tự xây cỗ máy phía sau nó. Bài này viết lại cả ba hook bằng vài chục dòng JavaScript thuần — chạy được ngay trong trang — và từng luật tự nhiên sáng tỏ.
1. Ba câu hỏi mà “cách dùng” không trả lời
Các bài về useMemo/useCallback thường dạy bạn KHI NÀO nên memo hóa, đo bằng Profiler, hay bật React Compiler. Đó là câu chuyện quan trọng — nhưng nó bỏ ngỏ ba câu hỏi cơ chế:
- Vì sao gọi hook trong
if,forhay sau mộtreturnsớm lại làm hỏng component? - Deps array
[a, b]được so sánh với deps của lần render TRƯỚC bằng cách nào, khi mỗi lần render là một lời gọi hàm mới? - Vì sao
useReftrả về đúng một object suốt vòng đời component, cònuseMemothì “đôi khi” tính lại?
Cách trả lời chắc nhất không phải đọc thêm docs, mà là tự viết lại ba hook đó. Cỗ máy hóa ra chỉ là một mảng ô nhớ và một con trỏ (cursor). Khi bạn thấy cái máy, cả ba câu hỏi trên tự tan.
Lưu ý
Bài này là mô hình DẠY HỌC, không phải source code React. React thật lưu state của hook trong một linked list tên memoizedState gắn trên fiber của từng component instance, kèm scheduler và batching. Mình dùng mảng + cursor cho dễ nhìn; mục 8 sẽ nói rõ cái mini này thiếu gì so với React thật.
2. Mảng ô nhớ và cursor: mini runtime
Ý tưởng gốc: mỗi component instance có một mảng các ô nhớ hook, và một cursor tăng dần theo THỨ TỰ GỌI hook trong một lần render. Không có tên biến, không có magic — hook thứ nhất luôn đọc ô 0, hook thứ hai luôn đọc ô 1, cứ thế.
Hàm render làm đúng hai việc: reset cursor về 0, rồi gọi component. Mảng ô nhớ nằm ngoài render nên nó persist giữa các lần render.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const cells = []; // ô nhớ hook, persist giữa các lần render
let cursor = 0; // con trỏ, reset về 0 mỗi lần render
let current = null;
function render(Component) {
cursor = 0; // luôn bắt đầu đọc ô từ index 0
current = Component;
return Component();
}
// myUseState tối giản — chỉ để DRIVE demo, không phải bài về state
function myUseState(initial) {
const i = cursor;
if (cells[i] === undefined) {
cells[i] = [initial, (next) => { cells[i][0] = next; render(current); }];
}
cursor++;
return cells[i];
}
function App() {
const [a] = myUseState("A");
const [b] = myUseState("B");
console.log("ô 0 ->", a, "| ô 1 ->", b);
}
render(App);Chạy thử, console in:
1
ô 0 -> A | ô 1 -> BGiải thích:
myUseStateđọc ô ở vị trícursorhiện tại, rồicursor++để hook kế tiếp đọc ô tiếp theo.- Lần đầu ô còn
undefinednên nó khởi tạo[value, setter]; các lần sau ô đã có, trả nguyên giá trị cũ. - Cái quyết định “hook nào đọc ô nào” chỉ là thứ tự gọi, không phải tên biến
a,b. Nhớ điều này — mục 6 sẽ dựa hết vào nó.
View Mermaid diagram code
flowchart LR
R["render(Component)"] --> C["cursor = 0"]
C --> H1["hook #1 đọc ô[0], cursor++"]
H1 --> H2["hook #2 đọc ô[1], cursor++"]
H2 --> H3["hook #3 đọc ô[2], cursor++"]
style R fill:#0e2233,stroke:#4aa8ff,stroke-width:1.5px,color:#9ecbff
style C fill:#2a2008,stroke:#ffc043,stroke-width:1.5px,color:#ffd98a
style H1 fill:#1a2c08,stroke:#c7ff50,stroke-width:1.5px,color:#c7ff50
style H2 fill:#1a2c08,stroke:#c7ff50,stroke-width:1.5px,color:#c7ff50
style H3 fill:#1a2c08,stroke:#c7ff50,stroke-width:1.5px,color:#c7ff50Mảng ô nhớ mới chỉ GIỮ giá trị; thứ biến useState thành state thật là cái setter. Setter ghi đè ô nhớ rồi gọi lại render — đó là “phản hồi” khép vòng: đổi state thì component chạy lại và đọc ra giá trị mới.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const cells = [];
let cursor = 0;
let current = null;
function render(Component) { cursor = 0; current = Component; return Component(); }
function myUseState(initial) {
const i = cursor;
if (cells[i] === undefined) {
cells[i] = [initial, (next) => { cells[i][0] = next; render(current); }];
}
cursor++;
return cells[i];
}
let saved;
function App() {
const [count, setCount] = myUseState(0);
saved = setCount;
console.log("render, count =", count);
}
render(App); // render lần đầu
saved(5); // gọi setState → ghi ô nhớ rồi render lạiConsole in:
1
2
render, count = 0
render, count = 5saved(5) làm đúng hai việc trong một dòng của setter: ghi 5 vào ô 0, rồi render(current) chạy lại App. Lần chạy thứ hai đọc lại ô 0 — giờ là 5 — nên component “phản hồi” state mới. Giữ ý này cho mục 3: bỏ setter đi thì mutate ô nhớ không kéo theo render nào.
3. myUseRef: ô nhớ không có setter
useRef là hook đơn giản nhất để bắt đầu, vì nó chỉ làm một việc: tạo object { current } đúng MỘT LẦN, rồi mọi render sau trả về ĐÚNG object đó.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const cells = [];
let cursor = 0;
function render(Component) { cursor = 0; return Component(); }
function myUseRef(initial) {
const i = cursor;
if (cells[i] === undefined) cells[i] = { current: initial };
cursor++;
return cells[i];
}
function Counter() {
const renderCount = myUseRef(0);
renderCount.current++;
console.log("render #", renderCount.current, "— cùng một ô nhớ qua các render");
}
render(Counter);
render(Counter);
render(Counter);Console in:
1
2
3
render # 1 — cùng một ô nhớ qua các render
render # 2 — cùng một ô nhớ qua các render
render # 3 — cùng một ô nhớ qua các renderrenderCount.current cộng dồn qua ba lần render, chứng tỏ đó luôn là một object duy nhất. Từ đây rút ra hai điều mà bạn hay nghe nhưng giờ mới thấy lý do:
useRef≈ ô nhớ không có setter. So vớimyUseState,myUseRefbỏ hẳn phần setter. Không setter nghĩa là mutate.currentkhông gọirender— nên đổiref.currentkhông schedule render nào cả. Đó là lý do ref hợp để giữ giá trị “bên lề” không cần vẽ lại UI.useRef(x)về mặt khái niệm tương đươnguseState({ current: x })[0]— cùng là một ô nhớ persist, chỉ khác là bạn tự mutate.currentthay vì gọi setter.
4. myUseMemo: cache theo deps
useMemo thêm đúng một ý so với useRef: ô nhớ giờ giữ cặp [value, deps] của lần trước. Trước khi tính lại, nó so deps mới với deps cũ; giống nhau thì trả cache, khác thì tính lại và lưu đè.
Phần “so deps” chính là areDepsEqual: cùng length, và Object.is từng phần tử — đúng thuật toán React dùng.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const cells = [];
let cursor = 0;
function render(Component) { cursor = 0; return Component(); }
function areDepsEqual(prev, next) {
if (prev === undefined || next === undefined) return false;
if (prev.length !== next.length) return false;
for (let i = 0; i < prev.length; i++) {
if (!Object.is(prev[i], next[i])) return false;
}
return true;
}
function myUseMemo(factory, deps) {
const i = cursor;
const prev = cells[i]; // [value, deps] của lần trước
if (prev && areDepsEqual(prev[1], deps)) { cursor++; return prev[0]; }
const value = factory(); // deps đổi (hoặc lần đầu) → tính lại
cells[i] = [value, deps];
cursor++;
return value;
}
function expensive(n) {
console.log("computing...", n);
return n * 2;
}
function Component(n) {
return myUseMemo(() => expensive(n), [n]);
}
console.log("kết quả:", render(() => Component(5)));
console.log("kết quả:", render(() => Component(5)));
console.log("kết quả:", render(() => Component(9)));Console in:
1
2
3
4
5
computing... 5
kết quả: 10
kết quả: 10
computing... 9
kết quả: 18Giải thích:
- Render đầu: ô trống → gọi
factory, incomputing... 5, lưu[10, [5]]. - Render hai: deps
[5]khớp[5]→ trả thẳng cache10, KHÔNG incomputing. - Render ba: deps
[9]khác[5]→ tính lại, incomputing... 9.
Dòng computing... chỉ hiện khi deps thực sự đổi — đó là toàn bộ giá trị của useMemo. Và giờ bạn thấy so sánh deps xảy ra ở đâu: Object.is trên từng phần tử, không phải so sâu (deep equal).
4.1. Năm useMemo, năm ô nhớ riêng
Câu hỏi hay gặp: một component gọi useMemo ở NĂM chỗ khác nhau thì cỗ máy biết đâu là cache và deps của TỪNG cái bằng cách nào? Trả lời vẫn chỉ là cursor. Lời gọi thứ nhất đọc ô 0, thứ hai ô 1, … thứ năm ô 4 — nên cells giữ năm cặp [value, deps] độc lập. Đổi một input chỉ làm đúng memo phụ thuộc input đó tính lại; bốn cái còn lại trả cache.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const cells = [];
let cursor = 0;
function render(Component) { cursor = 0; return Component(); }
function areDepsEqual(prev, next) {
if (prev === undefined || next === undefined) return false;
if (prev.length !== next.length) return false;
for (let i = 0; i < prev.length; i++) if (!Object.is(prev[i], next[i])) return false;
return true;
}
function myUseMemo(factory, deps) {
const i = cursor;
const prev = cells[i];
if (prev && areDepsEqual(prev[1], deps)) { cursor++; return prev[0]; }
const value = factory();
cells[i] = [value, deps];
cursor++;
return value;
}
function Invoice(price, qty, name, rate, code) {
const double = myUseMemo(() => { console.log("• memo#1 price"); return price * 2; }, [price]); // ô 0
const plus = myUseMemo(() => { console.log("• memo#2 qty"); return qty + 1; }, [qty]); // ô 1
const upper = myUseMemo(() => { console.log("• memo#3 name"); return name.toUpperCase(); }, [name]); // ô 2
const ratio = myUseMemo(() => { console.log("• memo#4 rate"); return rate / 100; }, [rate]); // ô 3
const tag = myUseMemo(() => { console.log("• memo#5 code"); return "#" + code; }, [code]); // ô 4
console.log(`= ${double} ${plus} ${upper} ${ratio} ${tag}`);
}
render(() => Invoice(100, 2, "an", 8, 7)); // render 1: cả 5 tính lần đầu
render(() => Invoice(100, 2, "an", 8, 7)); // render 2: mọi deps giữ nguyên → cache hết
render(() => Invoice(100, 5, "an", 8, 7)); // render 3: chỉ qty đổi 2 → 5Console in:
1
2
3
4
5
6
7
8
9
• memo#1 price
• memo#2 qty
• memo#3 name
• memo#4 rate
• memo#5 code
= 200 3 AN 0.08 #7
= 200 3 AN 0.08 #7
• memo#2 qty
= 200 6 AN 0.08 #7Render 1 tính cả năm (ô còn trống). Render 2 không in dòng • memo nào — mọi deps giữ nguyên nên năm ô trả cache. Render 3 chỉ đổi qty (2 → 5), và chỉ memo#2 (ô 1) tính lại; bốn ô kia vẫn khớp deps nên giữ cache. Mỗi memo soi đúng deps của riêng nó vì nó luôn ngồi đúng một ô cố định.
Luật “một lời gọi, một ô” này không riêng gì useMemo — nó đúng cho mọi hook, vì tất cả đều đọc cursor y hệt. Năm useRef là năm object riêng:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const cells = [];
let cursor = 0;
function render(Component) { cursor = 0; return Component(); }
function myUseRef(initial) {
const i = cursor;
if (cells[i] === undefined) cells[i] = { current: initial };
cursor++;
return cells[i];
}
function Row() {
const refs = [myUseRef("a"), myUseRef("b"), myUseRef("c"), myUseRef("d"), myUseRef("e")];
console.log("5 ref là 5 object khác nhau?", new Set(refs).size === 5);
}
render(Row);Console in:
1
5 ref là 5 object khác nhau? trueCòn useCallback thì khỏi cần chứng minh riêng: nó chính là useMemo (mục 5), nên năm useCallback cũng là năm ô cache hàm độc lập.
5. myUseCallback chỉ là một dòng
useCallback không phải hook mới — nó là useMemo mà giá trị được cache chính là hàm bạn truyền vào:
1
const myUseCallback = (fn, deps) => myUseMemo(() => fn, deps);Nghĩa là useCallback(fn, deps) ≡ useMemo(() => fn, deps). useMemo cache kết quả gọi factory; ở đây factory trả về chính fn, nên cái được cache là reference của hàm. Chứng minh bằng cách so === reference giữa hai lần render:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const cells = [];
let cursor = 0;
function render(Component) { cursor = 0; return Component(); }
function areDepsEqual(prev, next) {
if (prev === undefined || next === undefined) return false;
if (prev.length !== next.length) return false;
for (let i = 0; i < prev.length; i++) if (!Object.is(prev[i], next[i])) return false;
return true;
}
function myUseMemo(factory, deps) {
const i = cursor;
const prev = cells[i];
if (prev && areDepsEqual(prev[1], deps)) { cursor++; return prev[0]; }
const value = factory();
cells[i] = [value, deps];
cursor++;
return value;
}
const myUseCallback = (fn, deps) => myUseMemo(() => fn, deps);
function Component(label) {
return myUseCallback(() => label, [label]);
}
const cb1 = render(() => Component("a"));
const cb2 = render(() => Component("a"));
console.log("cùng deps → cùng reference?", cb1 === cb2);
const cb3 = render(() => Component("b"));
console.log("deps đổi → reference mới?", cb2 !== cb3);Console in:
1
2
cùng deps → cùng reference? true
deps đổi → reference mới? trueReference giữ nguyên khi deps không đổi — đúng thứ bạn cần khi truyền callback xuống một component con đang so props bằng reference. Nhưng chú ý: đây mới là cơ chế; còn KHI NÀO việc giữ reference thật sự loại bỏ được render thừa là chuyện của một bài khác (mình quay lại ở mục 8).
Ghép cả bốn hook lại, đây là toàn bộ mini runtime — chính là phần runtime của file mini-hooks/mini-hooks.js trong repo ví dụ, ở đây mình thêm một demo nhỏ ở cuối để bấm Run xem output:
▶ Chạy thử ở máy bạn: mini-hooks/mini-hooks.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// ===== mini runtime =====
const cells = [];
let cursor = 0;
let current = null;
function render(Component) {
cursor = 0;
current = Component;
return Component();
}
function myUseState(initial) {
const i = cursor;
if (cells[i] === undefined) {
cells[i] = [initial, (next) => { cells[i][0] = next; render(current); }];
}
cursor++;
return cells[i];
}
function myUseRef(initial) {
const i = cursor;
if (cells[i] === undefined) cells[i] = { current: initial };
cursor++;
return cells[i];
}
function areDepsEqual(prev, next) {
if (prev === undefined || next === undefined) return false;
if (prev.length !== next.length) return false;
for (let i = 0; i < prev.length; i++) if (!Object.is(prev[i], next[i])) return false;
return true;
}
function myUseMemo(factory, deps) {
const i = cursor;
const prev = cells[i];
if (prev && areDepsEqual(prev[1], deps)) { cursor++; return prev[0]; }
const value = factory();
cells[i] = [value, deps];
cursor++;
return value;
}
const myUseCallback = (fn, deps) => myUseMemo(() => fn, deps);
// ===== dùng cả bốn hook cùng lúc =====
function Product(price) {
const renders = myUseRef(0);
renders.current++;
const withTax = myUseMemo(() => {
console.log("tính lại thuế cho giá", price);
return Math.round(price * 1.1);
}, [price]);
console.log(`render #${renders.current} | giá ${price} | gồm thuế ${withTax}`);
}
render(() => Product(100));
render(() => Product(100));
render(() => Product(200));Console in:
1
2
3
4
5
tính lại thuế cho giá 100
render #1 | giá 100 | gồm thuế 110
render #2 | giá 100 | gồm thuế 110
tính lại thuế cho giá 200
render #3 | giá 200 | gồm thuế 220renders.current cộng dồn qua ba render (ref persist), còn withTax chỉ tính lại khi price đổi (memo theo deps) — bốn hook, một mảng ô nhớ.
6. Hook trong if: hai ô tráo cho nhau
Đây là demo đinh của bài. “Không được gọi hook trong if“ nghe như một quy ước tùy tiện — cho tới khi bạn thấy nó là hệ quả cơ học của mảng + cursor.
Nhớ lại: hook đọc ô theo cursor, và cursor tăng theo thứ tự gọi. Nếu một render BỎ QUA một hook, mọi hook sau nó bị lệch một ô — và đọc nhầm dữ liệu của hook khác.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const cells = [];
let cursor = 0;
function render(Component) { cursor = 0; return Component(); }
function myUseState(initial) {
const i = cursor;
if (cells[i] === undefined) cells[i] = [initial, () => {}];
cursor++;
return cells[i];
}
let showEmail = true;
function Profile() {
if (showEmail) {
const [email] = myUseState("alice@example.com"); // render đầu: chiếm ô 0
console.log(" email ô:", email);
}
const [theme] = myUseState("dark"); // ĐỊNH là ô 1
console.log(" theme ô:", theme);
}
console.log("render 1 (showEmail = true):");
render(Profile);
showEmail = false;
console.log("render 2 (showEmail = false):");
render(Profile);Console in:
1
2
3
4
5
render 1 (showEmail = true):
email ô: alice@example.com
theme ô: dark
render 2 (showEmail = false):
theme ô: alice@example.comĐọc kỹ dòng cuối. Render 2 bỏ qua if, nên myUseState("dark") giờ là hook ĐẦU TIÊN được gọi → nó đọc ô 0, mà ô 0 đang giữ "alice@example.com" từ render trước. theme nhận nhầm giá trị của email. Chạy lại bao nhiêu lần cũng ra đúng kết quả tráo đó — đây là lỗi cơ học, không phải ngẫu nhiên theo timing.
Vậy Rules of Hooks không phải quy ước lịch sự — nó là điều kiện để cursor luôn khớp ô qua mọi render. Gọi hook cố định về số lượng và thứ tự thì ô mới không bao giờ lệch. Đây chính là câu trả lời mà mọi bài “cách dùng” còn nợ bạn.
7. Hai bẫy deps suy ra từ cỗ máy
Khi đã thấy areDepsEqual dùng Object.is từng phần tử, hai lỗi deps kinh điển không còn là điều phải học thuộc — chúng suy ra thẳng từ code.
7.1. Object/array literal đặt thẳng trong deps
Mỗi lần render, một { ... } hay [ ... ] viết trong thân component là một reference mới. Object.is giữa hai object khác reference luôn false, nên deps “khác” mỗi render → cache vô dụng, factory chạy lại liên tục.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const cells = [];
let cursor = 0;
function render(Component) { cursor = 0; return Component(); }
function areDepsEqual(prev, next) {
if (prev === undefined || next === undefined) return false;
if (prev.length !== next.length) return false;
for (let i = 0; i < prev.length; i++) if (!Object.is(prev[i], next[i])) return false;
return true;
}
function myUseMemo(factory, deps) {
const i = cursor;
const prev = cells[i];
if (prev && areDepsEqual(prev[1], deps)) { cursor++; return prev[0]; }
const value = factory();
cells[i] = [value, deps];
cursor++;
return value;
}
function Search(query) {
const filters = { q: query }; // object literal MỚI mỗi render
return myUseMemo(() => {
console.log("computing cho:", filters.q);
return filters.q.toUpperCase();
}, [filters]); // Object.is fail mỗi lần
}
render(() => Search("react"));
render(() => Search("react")); // cùng query nhưng vẫn tính lạiConsole in:
1
2
computing cho: react
computing cho: reactCùng query nhưng computing in hai lần, vì filters là object mới ở mỗi render. Cách sửa: đưa giá trị nguyên thủy vào deps ([query]), hoặc memo hóa chính filters. Đây cũng đúng là lý do “props tạo mới mỗi render làm React.memo vô dụng” — cùng một cơ chế Object.is, chỉ khác chỗ so sánh.
7.2. Thiếu dep: closure cũ bị giam trong ô nhớ
Nếu deps thiếu một giá trị mà hàm có dùng, cache trả lại hàm CŨ — và hàm cũ đóng (closure) trên giá trị của lần render tạo ra nó.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const cells = [];
let cursor = 0;
function render(Component) { cursor = 0; return Component(); }
function areDepsEqual(prev, next) {
if (prev === undefined || next === undefined) return false;
if (prev.length !== next.length) return false;
for (let i = 0; i < prev.length; i++) if (!Object.is(prev[i], next[i])) return false;
return true;
}
function myUseMemo(factory, deps) {
const i = cursor;
const prev = cells[i];
if (prev && areDepsEqual(prev[1], deps)) { cursor++; return prev[0]; }
const value = factory();
cells[i] = [value, deps];
cursor++;
return value;
}
const myUseCallback = (fn, deps) => myUseMemo(() => fn, deps);
function Greeter(name) {
return myUseCallback(() => console.log("Xin chào,", name), []); // THIẾU dep `name`
}
const g1 = render(() => Greeter("An"));
const g2 = render(() => Greeter("Bình")); // name mới, nhưng deps rỗng
g2(); // in tên CŨ vì hàm bị giam trong ô nhớ từ render đầuConsole in:
1
Xin chào, Ang2 gọi ra "An" dù render thứ hai truyền "Bình" — vì deps [] không bao giờ đổi nên myUseCallback cứ trả hàm của render đầu, hàm đó khóa cứng name = "An". Đây chính là stale closure: sửa bằng cách khai đúng [name] trong deps để render sau tạo hàm mới.
8. Mini khác React thật ở đâu
Cỗ máy mảng + cursor giải thích được LUẬT, nhưng đừng nhầm nó với React. Những chỗ khác biệt quan trọng:
- Per-instance state. Mini dùng một mảng
cellsở module-scope, nên chỉ mô phỏng được một component. React lưu ô nhớ trên fiber riêng của từng instance (linked listmemoizedState), nên hai<Counter />không đụng ô của nhau. - Scheduling và batching.
myUseStatecủa mình gọirenderngay lập tức, đồng bộ. React gom nhiềusetState, ưu tiên, và có thể ngắt/nối lại việc render (concurrent). useMemolà tối ưu, không phải bảo đảm. Theo react.dev, React có thể vứt cache vì lý do riêng: trong development khi bạn sửa file component; cả dev lẫn production nếu component suspend ở lần mount đầu; và trong tương lai có thể thêm trường hợp mới (ví dụ list ảo hóa). Ngoài ra, ở StrictMode (chỉ development), React gọi hàm tính củauseMemohai lần mỗi khi deps đổi để phát hiện hàm không thuần. Kết luận thực dụng: đừng dùnguseMemocho tính đúng đắn ngữ nghĩa (semantic correctness) — chỉ dùng nó như một tối ưu hiệu năng. Cần đảm bảo giá trị ổn định thìuseStatehoặcuseRefmới đúng công cụ.
Ở thời điểm viết, React stable đang ở major 19. Cơ chế mảng-ô-nhớ-theo-thứ-tự thì đúng từ React 16.8 tới nay; các con số fiber/scheduler là chi tiết nội bộ, có thể đổi giữa các phiên bản.
Ranh giới của bài đến đây là rõ: bạn vừa thấy vì sao cỗ máy chạy như thế. Còn khi nào nên memo hóa, đo render thừa bằng React DevTools Profiler, hay để React Compiler tự chèn memo — đó là câu chuyện “cách dùng”, xứng đáng một bài riêng. Next step cụ thể: mở React DevTools trên một component thật, xem tab hooks của nó — giờ bạn đã biết cái danh sách đó chính là mảng ô nhớ mà mình vừa dựng lại.