Home / Frontend / Tự viết lại useMemo, useCallback, useRef: hiểu hooks từ gốc
Frontend

Tự viết lại useMemo, useCallback, useRef: hiểu hooks từ gốc

Re-implement useMemo, useCallback, useRef bằng JavaScript thuần: hiểu deps array, cursor và Rules of Hooks từ gốc

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ế:

  1. Vì sao gọi hook trong if, for hay sau một return sớm lại làm hỏng component?
  2. 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?
  3. Vì sao useRef trả về đúng một object suốt vòng đời component, còn useMemo thì “đô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ớ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 -> B

Giải thích:

  • myUseState đọc ô ở vị trí cursor hiện tại, rồi cursor++ để hook kế tiếp đọc ô tiếp theo.
  • Lần đầu ô còn undefined nê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:#c7ff50

Mả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ại

Console in:

1
2
render, count = 0
render, count = 5

saved(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 render

renderCount.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ới myUseState, myUseRef bỏ hẳn phần setter. Không setter nghĩa là mutate .current không gọi render — nên đổi ref.current khô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 đương useState({ current: x })[0] — cùng là một ô nhớ persist, chỉ khác là bạn tự mutate .current thay 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ả: 18

Giải thích:

  • Render đầu: ô trống → gọi factory, in computing... 5, lưu [10, [5]].
  • Render hai: deps [5] khớp [5] → trả thẳng cache 10, KHÔNG in computing.
  • Render ba: deps [9] khác [5] → tính lại, in computing... 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 → 5

Console 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 #7

Render 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#21) 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? true

Cò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? true

Reference 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ế 220

renders.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ại

Console in:

1
2
computing cho: react
computing cho: react

Cù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 đầu

Console in:

1
Xin chào, An

g2 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 list memoizedState), nên hai <Counter /> không đụng ô của nhau.
  • Scheduling và batching. myUseState của mình gọi render ngay lập tức, đồng bộ. React gom nhiều setState, ưu tiên, và có thể ngắt/nối lại việc render (concurrent).
  • useMemo là 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ủa useMemo hai 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ùng useMemo cho 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ì useState hoặc useRef mớ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.

← Previous
Lỗi CORS: hiểu same-origin policy và preflight request
Table of Contents
  1. 1. Ba câu hỏi mà “cách dùng” không trả lời
  2. 2. Mảng ô nhớ và cursor: mini runtime
  3. 3. myUseRef: ô nhớ không có setter
  4. 4. myUseMemo: cache theo deps
    1. 4.1. Năm useMemo, năm ô nhớ riêng
  5. 5. myUseCallback chỉ là một dòng
  6. 6. Hook trong if: hai ô tráo cho nhau
  7. 7. Hai bẫy deps suy ra từ cỗ máy
    1. 7.1. Object/array literal đặt thẳng trong deps
    2. 7.2. Thiếu dep: closure cũ bị giam trong ô nhớ
  8. 8. Mini khác React thật ở đâu