Skip to content

Lesson 0.5.5 — Callback ​

Bài 0.5.5 — Callback
Function as argument & Callback concept • 27 phút
0:00 / 0:00

0. Metadata ​

FieldValue
Stage0 — JavaScript Language Foundation
Module0.5 — Functions
Lesson0.5.5
CompetencyC01.5 — Functions
Depth TargetL2–L3
PrerequisitesFunction Expression (0.5.2), Arrow Functions (0.5.3), Parameters (0.5.4), Array Iteration (0.6.3)
Estimated Cognitive LoadMedium

1. Why This Exists (Vì sao cần học) ​

Bạn đã biết function là giá trị có thể gán vào biến. Nhưng trong thực tế, sức mạnh thực sự của "function as value" xuất hiện khi bạn đưa function đó cho một function khác để nó chạy hộ.

js
const approved = users.filter(user => user.age >= 18);

Vấn đề cốt lõi

filter không biết bạn muốn lọc theo tiêu chí nào. Bạn truyền vào một đoạn logic (user => user.age >= 18) và nói: "Hãy gọi lại (call back) đoạn logic này với từng phần tử."

Đây là callback pattern — một trong những pattern quan trọng nhất của JavaScript. Không hiểu callback, bạn không thể dùng map, filter, find, và sẽ hoàn toàn bị lạc khi bước vào async (Stage 3) hay React (Stage 8).

Ở bài này, bạn học callback ở dạng đồng bộ (synchronous): function A truyền function B vào function C, C gọi B ngay lập tức trong quá trình chạy. Callback bất đồng bộ (asynchronous) — như setTimeout, Promise — thuộc Stage 3.

2. Prerequisites (Yêu cầu đầu vào) ​

Trước khi học bài này, bạn cần:

  • Viết được function expression và arrow function (0.5.2–0.5.3).
  • Hiểu parameter là "slot nhận giá trị" và có thể nhận bất kỳ giá trị nào (0.5.4).
  • Biết array và object ở mức cơ bản: tạo, truy cập, lặp bằng for hoặc for...of (0.2.4, 0.4.5–0.4.6).
  • Hiểu return và undefined (0.5.1).

WARNING

Nếu bạn chưa chắc const fn = x => x * 2 tạo ra một giá trị có thể gán vào biến, quay lại 0.5.2. Callback chỉ là việc gán function value vào parameter của function khác.

INFO

Các ví dụ trong bài có thể đề cập đến map hoặc filter nhưng bạn không cần hiểu sâu internal của các array methods đó. Chúng chỉ đóng vai trò host function quen thuộc để minh họa callback pattern. Array methods sẽ được đào sâu ở 0.6.3.

3. Learning Objectives (Mục tiêu học tập) ​

Sau bài này, bạn có thể:

  1. Giải thích callback pattern: truyền một function vào function khác để nó được gọi lại.
  2. Viết một function nhận callback và gọi callback đó với dữ liệu.
  3. Phân biệt truyền fn (function value) và fn() (gọi function ngay lập tức).
  4. Dự đoán output khi callback không return trong map.
  5. Sử dụng callback để tách logic "lặp" khỏi logic "xử lý từng phần tử".

4. Mental Model (Mô hình tư duy) ​

Hãy hình dung một nhà máy chế biến:

text
Nhà máy (higher-order function)
      ↓
Nhận nguyên liệu: [táo, lê, cam]
      ↓
Nhận công thức: function gọtVỏ(tráiCây) { ... }
      ↓
Với mỗi trái cây:
  → Đưa vào công thức
  → Nhận lại kết quả
  → Làm tiếp
      ↓
Trả về: [táo đã gọt, lê đã gọt, cam đã gọt]

Mental Model

Callback Pattern
      ↓
Caller có dữ liệu + logic
      ↓
Caller truyền logic vào Host Function qua parameter
      ↓
Host Function kiểm soát:
  - Khi nào gọi callback?
  - Với argument nào?
  - Bao nhiêu lần?
      ↓
Callback thực thi → return kết quả → Host Function tiếp tục

Quan trọng: Khi bạn truyền callback, bạn trao quyền kiểm soát cho host function. Host function quyết định thời điểm và cách thức gọi lại logic của bạn.

Ở Stage 0, host function gọi callback ngay lập tức và đồng bộ. Ở Stage 3, host function có thể gọi callback sau một khoảng thời gian.

5. Core Concepts (Các khái niệm cốt lõi) ​

Essential (Bắt buộc) ​

ConceptÝ nghĩa
CallbackFunction được truyền như argument vào function khác, để được gọi lại (call back) trong body của function đó.
Host FunctionFunction nhận callback làm parameter và quyết định khi nào gọi nó.
Passing vs Callinghost(fn) — truyền giá trị. host(fn()) — gọi ngay lập tức rồi truyền kết quả.
Synchronous CallbackCallback được gọi ngay trong quá trình host function chạy (ví dụ: map, filter).

Supporting (Hỗ trợ) ​

ConceptÝ nghĩa
Callback signatureHost function quyết định callback nhận bao nhiêu param và thứ tự (ví dụ: map truyền element, index, array).
Inline callbackViết callback trực tiếp tại nơi gọi: arr.map(x => x * 2).
Named callbackGán callback vào biến trước rồi truyền: const double = x => x * 2; arr.map(double);.

Awareness (Biết tồn tại) ​

ConceptLý do chưa đào sâu
Asynchronous callbacksetTimeout, fetch, event listener — callback chạy sau. Thuộc Stage 3 (Async & Concurrency).
Error-first callbackNode.js pattern (err, data) => {}. Sẽ gặp lại ở Stage 3/5.
Callback hellNesting nhiều callback. Giải pháp là Promise (Stage 3).

Out of Scope (Không thuộc bài này) ​

  • Event Loop / task scheduling (Stage 3)
  • Closure / lexical scope retention (Stage 1)
  • this binding trong callback (Stage 2)
  • Promise / async-await (Stage 3)
  • Higher-order function trả về function (0.5.6)

6. Worked Example (Ví dụ phân tích từng bước) ​

Xét đoạn code sau:

js
function processItems(items, action) {
  const results = [];
  for (let i = 0; i < items.length; i++) {
    const transformed = action(items[i], i);
    results.push(transformed);
  }
  return results;
}

const prices = [100, 200, 300];
const withTax = processItems(prices, function(price, index) {
  return price * 1.1;
});

console.log(withTax);

Walkthrough

Step 1 — Caller chuẩn bịprices là mảng dữ liệu. function(price, index) { return price * 1.1; } là callback — một function value được tạo inline.

Step 2 — Truyền vào Host FunctionprocessItems(prices, callback) gọi host function. items nhận [100, 200, 300]. action nhận function value.

Step 3 — Host Function kiểm soát luồngprocessItems tạo vòng lặp. Nó quyết định:

  • Lặp bao nhiêu lần? = items.length.
  • Truyền gì vào callback? = items[i] và i.
  • Khi nào gọi? = Ngay trong vòng lặp, đồng bộ.

Step 4 — Callback thực thi Lần 1: action(100, 0) → return 110. Lần 2: action(200, 1) → return 220. Lần 3: action(300, 2) → return 330.

Step 5 — Kết quả tập hợpresults = [110, 220, 330]. Host function trả về mảng này.

Key Insight: processItems không biết logic cụ thể. Nó chỉ biết: "Tôi có một đống item. Tôi đưa từng item cho action. action trả về cái gì, tôi gom lại." Đây là sự phân tách trách nhiệm (separation of concerns).

7. Prediction Exercise (Bài tập dự đoán) ​

Đừng chạy code. Đọc và dự đoán output hoặc lỗi, sau đó giải thích tại sao.

Câu 1 ​

js
function run(fn) {
  return fn();
}
console.log(run(() => 5));

Câu 2 ​

js
function run(fn) {
  return fn();
}
console.log(run(console.log("hello")));

Câu 3 ​

js
function mapLike(arr, transform) {
  const out = [];
  for (const item of arr) {
    transform(item);
    out.push(item);
  }
  return out;
}
const result = mapLike([1, 2, 3], x => x * 10);
console.log(result);

Câu 4 ​

js
function greetUser(name, formatter) {
  return formatter(name);
}
const result = greetUser("An", name => "Hi, " + name);
console.log(result);
[Đáp án & Giải thích]

Câu 1: 5

  • Giải thích: run nhận arrow function () => 5 qua parameter fn. fn() gọi arrow function, trả về 5. run trả về 5.

Câu 2: "hello" rồi TypeError: fn is not a function

  • Giải thích: console.log("hello") được evaluate trước khi truyền vào run. Nó in "hello" ra console và trả về undefined. run nhận undefined qua fn. fn() cố gọi undefined → TypeError.

Câu 3: [1, 2, 3]

  • Giải thích: Callback x => x * 10 được gọi (transform(item)), nhưng kết quả x * 10 bị bỏ đi — không được push vào out. out.push(item) đẩy giá trị gốc. Đây là bug "gọi callback nhưng quên dùng kết quả".

Câu 4: "Hi, An"

  • Giải thích: formatter nhận arrow function name => "Hi, " + name. formatter(name) gọi callback với "An", trả về "Hi, An".

8. Implementation Lab (Bài lab thực hành) ​

Level 1 — Guided (Có hướng dẫn) ​

Viết function repeat nhận count và action (callback). Gọi action đúng count lần, mỗi lần truyền vào số thứ tự (bắt đầu từ 1).

js
function repeat(count, action) {
  // Gọi action(i) với i từ 1 đến count
}

repeat(3, n => console.log("Lần " + n));
// Expected:
// Lần 1
// Lần 2
// Lần 3
[Đáp án tham khảo]
js
function repeat(count, action) {
  for (let i = 1; i <= count; i++) {
    action(i);
  }
}

repeat(3, n => console.log("Lần " + n));

// Expected:
// Lần 1
// Lần 2
// Lần 3

Giải thích:

  • Dùng vòng lặp for để chạy từ 1 đến count.
  • Mỗi lần lặp, gọi callback action(i). i chính là số thứ tự được truyền vào callback.

Level 2 — Partial Scaffold (Khung sẵn) ​

Hoàn thành function filterBy — phiên bản đơn giản của Array.prototype.filter:

js
function filterBy(items, test) {
  const passed = [];
  for (const item of items) {
    // Nếu test(item) trả về truthy, thêm item vào passed
  }
  return passed;
}

// Sử dụng:
const adults = filterBy([
  { name: "A", age: 17 }, 
  { name: "B", age: 20 }
  ], /* callback */);
console.log(adults); // [{ name: "B", age: 20 }]
[Đáp án tham khảo]
js
function filterBy(items, test) {
  const passed = [];

  for (const item of items) {
    if (test(item)) {
      passed.push(item);
    }
  }

  return passed;
}

const adults = filterBy(
  [
    { name: "A", age: 17 },
    { name: "B", age: 20 }
  ],
  item => item.age >= 18
);

console.log(adults);
// [{ name: "B", age: 20 }]

Giải thích:

  • passed là mảng dùng để lưu các phần tử đạt điều kiện.
  • Với mỗi item, gọi test(item). Nếu kết quả là truthy, thêm item vào passed bằng push().
  • Cuối cùng return passed.

Level 3 — Independent (Tự viết) ​

Viết function processPipeline nhận một giá trị ban đầu và một mảng các callback. Mỗi callback nhận giá trị hiện tại và trả về giá trị mới. Trả về giá trị cuối cùng sau khi chạy qua tất cả callback.

js
// Ví dụ:
const result = processPipeline(5, [
  x => x * 2,
  x => x + 10,
  x => x.toString()
]);
console.log(result); // "20"

Gợi ý

Dùng value = fn(value) trong vòng lặp để cập nhật giá trị qua từng bước. Đây là nền tảng của functional pipeline pattern.

[Đáp án tham khảo]
js
function processPipeline(value, callbacks) {
  for (const fn of callbacks) {
    value = fn(value);
  }

  return value;
}

const result = processPipeline(5, [
  x => x * 2,
  x => x + 10,
  x => x.toString()
]);

console.log(result);
// "20"

Giải thích:

Giá trị được truyền lần lượt qua từng callback:

text
5
→ x => x * 2
→ 10
→ x => x + 10
→ 20
→ x => x.toString()
→ "20"
  • value = fn(value) cập nhật giá trị sau mỗi bước.
  • Callback tiếp theo sẽ nhận kết quả của callback trước đó.

Sau khi chạy hết các callback, return value trả về kết quả cuối cùng.

9. Edge Cases (Các trường hợp ngoại lệ) ​

Truyền fn() thay vì fn

js
function execute(callback) {
  return callback();
}
execute(console.log("Oops"));

Tại sao: console.log("Oops") chạy ngay lập tức khi evaluate argument. Kết quả (undefined) được truyền vào execute. execute cố gọi undefined.

Cách nhận biết: TypeError callback is not a function. Console đã in giá trị trước khi lỗi xuất hiện.

Cách xử lý: Truyền function value: execute(() => console.log("OK")) hoặc execute(myFunction).

Callback không return

js
const mapped = [1, 2, 3].map(x => {
  x * 2;
});

Tại sao: Arrow function có block body {} nhưng không có return. map gom kết quả của callback — ở đây là undefined.

Cách nhận biết: Kết quả là [undefined, undefined, undefined].

Cách xử lý: Dùng concise body x => x * 2 hoặc thêm return.

Callback không phải function

js
function run(fn) { fn(); }
run(null);

Tại sao: null không callable.

Cách nhận biết: TypeError fn is not a function.

Cách xử lý: Validation: if (typeof fn !== "function") throw new TypeError("Expected a function");

Host function quyết định signature

js
function process(data, cb) {
  cb(data);
}
process("hello", (data, index) => console.log(index));

Tại sao: Host function process chỉ truyền một argument (data). Callback nhận index nhưng nó là undefined.

Cách nhận biết: Parameter thứ hai luôn undefined dù tên có vẻ hợp lý.

Cách xử lý: Đọc documentation của host function để biết nó truyền gì vào callback. Không đoán.

10. Debug Lab (Bài lab gỡ lỗi) ​

Symptom (Triệu chứng): TypeError: callback is not a function

js
function fetchAndProcess(url, callback) {
  const data = { id: 1, name: "Test" };
  callback(data);
}

function handleData(data) {
  console.log("Received:", data.name);
}

fetchAndProcess("/api", handleData());

Reproduction (Tái hiện lỗi): Chạy code trên.

Evidence (Bằng chứng):

  • Lỗi xuất hiện tại dòng callback(data) trong fetchAndProcess.
  • Console có thể đã in "Received: Test" trước lỗi.

Hypothesis (Giả thuyết): handleData() (có dấu ngoặc đơn) được gọi ngay lập tức tại nơi truyền argument. Nó in ra console và trả về undefined. fetchAndProcess nhận undefined qua callback, sau đó cố gọi undefined(data).

Verification (Xác minh): Bỏ dấu ngoặc đơn: fetchAndProcess("/api", handleData). Code chạy đúng.

Root Cause (Nguyên nhân gốc rễ): Nhầm lẫn giữa truyền function (handleData — đưa công thức) và gọi function (handleData() — chạy công thức ngay và đưa kết quả).

Fix (Sửa):

js
fetchAndProcess("/api", handleData);

Prevention (Phòng ngừa):

  • Khi truyền callback, hỏi: "Tôi đang đưa function hay đang chạy function?"
  • Nếu thấy () ngay trong argument list, cảnh giác — đó thường là lỗi.
  • Dùng arrow function inline khi cần wrap: fetchAndProcess("/api", data => handleData(data)).

11. Design Exercise (Bài tập thiết kế giải pháp) ​

Bạn cần viết một hàm validate form. Có hai cách tiếp cận:

js
function validateForm(formData) {
  const errors = {};
  if (!formData.name || formData.name.length < 3) {
    errors.name = "Too short";
  }
  if (!formData.email || !formData.email.includes("@")) {
    errors.email = "Invalid email";
  }
  return errors;
}
js
function validateForm(formData, rules) {
  const errors = {};
  for (const [field, validator] of Object.entries(rules)) {
    const error = validator(formData[field]);
    if (error) errors[field] = error;
  }
  return errors;
}

// Sử dụng:
validateForm(data, {
  name: value => value?.length >= 3 ? null : "Too short",
  email: value => value?.includes("@") ? null : "Invalid email"
});

Câu hỏi:

  1. Option A có lợi gì? Khi nào nó phù hợp hơn?
  2. Option B có lợi gì? Nhược điểm gì?
  3. Nếu bạn cần thêm 10 field nữa, option nào dễ maintain hơn?
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. Option A đơn giản, dễ đọc, không cần hiểu callback. Phù hợp khi form có 1-2 field cố định.
    2. Option B tách rule ra khỏi engine. Dễ thêm/xóa field mà không sửa core logic. Nhược điểm: phức tạp hơn, cần hiểu callback pattern.
    3. Option B. Thêm field = thêm entry trong object rules, không cần đụng vào validateForm.
  • Kết luận: Callback pattern cho phép tách biệt "cái gì cần làm" khỏi "làm như thế nào". Đây là nền tảng của plugin architecture và validation libraries.

12. Production Scenario (Tình huống thực tế) ​

Context: Một junior refactor đoạn code sắp xếp sản phẩm theo giá:

js
const products = [
  { name: "A", price: 200 },
  { name: "B", price: 100 },
  { name: "C", price: 300 }
];

products.sort((a, b) => a.price > b.price);
console.log(products);

Symptom: Thứ tự sắp xếp không ổn định. Trên Chrome thì đúng, trên Firefox thì sai. Đôi khi phần tử bị bỏ qua.

Constraint: Không được đổi cấu trúc dữ liệu. Chỉ sửa callback trong sort.

Câu hỏi:

  1. Tại sao a.price > b.price là callback sai cho sort?
  2. Array.prototype.sort mong đợi callback trả về gì? (số âm, 0, hay số dương)
  3. Sửa lại callback đúng.
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. a.price > b.price trả về boolean (true/false). sort không mong đợi boolean — nó mong đợi số. true được coerce thành 1, false thành 0. Điều này khiến thuật toán sắp xếp không nhận được tín hiệu đúng về thứ tự.
    2. sort callback cần trả về: < 0 (a trước b), 0 (giữ nguyên), > 0 (b trước a).
    3. Sửa thành:
      js
      products.sort((a, b) => a.price - b.price);
  • Bài học: Mỗi host function có contract riêng về callback signature. Không tuân thủ contract dẫn đến silent bug hoặc behavior không đồng nhất giữa engine.

13. AI-assisted Exercise (Bài tập với AI) ​

Level B — Challenge

  1. Tự trả lời trước: Giải thích trong 2 câu: tại sao setTimeout(callback(), 1000) là sai, còn setTimeout(callback, 1000) là đúng.
  2. Hỏi AI: "In JavaScript, should I write setTimeout(callback, 1000) or setTimeout(callback(), 1000)?"
  3. So sánh câu trả lời AI với câu của bạn. AI có giải thích rõ "evaluate immediately" không?
  4. Verify bằng MDN: tìm setTimeout trên MDN và đọc phần "The function you want to execute".

Gợi ý

AI thường trả lời đúng là callback (không dấu ngoặc), nhưng đôi khi chỉ nói "because you want to pass the function, not call it" mà không giải thích evaluation order — rằng callback() chạy NGAY LẬP TỨC khi JS evaluate argument list, trước cả khi setTimeout được gọi. Nếu AI không nhắc đến "evaluate argument list" hoặc "call stack", bạn đã tìm ra điểm mù.

Đáp án tham khảo
  • Bạn nghĩ: callback() gọi function ngay lập tức tại chỗ truyền argument. Kết quả của callback() (thường là undefined) được truyền vào setTimeout. setTimeout nhận undefined thay vì một function, nên không chạy được. callback (không dấu ngoặc) truyền chính function value, để setTimeout gọi nó sau.

  • AI trả lời (typical):

    • Use setTimeout(callback, 1000) to pass the function.
    • setTimeout(callback(), 1000) calls the function immediately.
    • The first argument should be a function reference.
  • So sánh: AI đúng về cách viết nhưng thường không giải thích khi nào callback() chạy. Nó không nhắc rằng callback() xuất hiện trên call stack NGAY TẠI THỜI ĐIỂM TRUYỀN ARGUMENT, không phải sau 1 giây.

  • Điểm AI nói sai hoặc quá mơ hồ: "Calls the function immediately" — mơ hồ. Junior có thể hiểu "immediately" là "ngay sau khi setTimeout chạy". Thực tế, nó chạy trong quá trình evaluate arguments, trước cả khi setTimeout được invoke.

  • Kết luận: Nếu bạn chỉ ra được evaluation order (argument evaluation trước function call), bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở Stage 1 (call stack) và Stage 3 (async scheduling).

14. Teach Back (Dạy lại) ​

Giả sử một junior developer hỏi bạn:

"Em hiểu function là giá trị, nhưng tại sao người ta phải truyền nó vào hàm khác làm gì? Sao không gọi trực tiếp cho nhanh?"

Hãy giải thích trong 2 phút, dùng ví dụ Array.prototype.map hoặc validation pipeline.

Mô phỏng
  • Bạn nói: Bạn gọi trực tiếp khi bạn biết chính xác cần làm gì ngay lúc viết code. Nhưng map không biết bạn muốn nhân đôi hay bình phương. Nó nói: "Tôi lo việc đi qua từng phần tử, còn việc biến đổi từng phần tử thì bạn đưa tôi một function — tôi sẽ gọi lại (call back) function đó." Điều này tách biệt "đi qua mảng" khỏi "xử lý từng item". Nhờ đó map viết một lần, dùng được mọi nơi. Còn nếu bạn gọi trực tiếp, bạn phải viết lại vòng lặp cho mỗi loại xử lý khác nhau.

💡 Tưởng tượng bạn đi ăn buffet. Bạn không vào bếp tự nấu (gọi function trực tiếp). Thay vào đó, bạn đưa đĩa cho đầu bếp (truyền callback) và nói: "Hãy gọi tôi khi có món này." Đầu bếp quyết định khi nào nấu xong và gọi bạn lại. Bạn không kiểm soát thời gian nấu, nhưng bạn kiểm soát bạn sẽ làm gì với món ăn.

Gợi ý đánh giá bản thân

  • Đồng nghiệp có hiểu tại sao "tách biệt" quan trọng không?
  • Bạn có tránh dùng từ "callback hell" hay "async" không? (Chưa học ở Stage 0)
  • Nếu đồng nghiệp hỏi: "Vậy khi nào KHÔNG nên dùng callback?" — bạn trả lời được không? (Gợi ý: khi logic quá đơn giản, viết trực tiếp rõ ràng hơn.)

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giáTask
Explain callback pattern (Giải thích callback pattern)Teach BackTeach Back Section
Distinguish fn vs fn() (Phân biệt fn và fn())Prediction (Dự đoán)Câu 2, Prediction Exercise
Write host function using callback (Viết host function dùng callback)Implementation (Thực hành)Implementation Lab Level 2
Debug immediate invocation bug (Gỡ lỗi gọi ngay lập tức)Debug LabDebug Lab Section
Choose inline vs callback architecture (Chọn inline hay callback)Design DecisionDesign Exercise

16. Exit Criteria (Tiêu chí qua bài) ​

  • [ ] Có thể giải thích callback pattern bằng ví dụ cụ thể.
  • [ ] Có thể viết một host function nhận callback và gọi callback với dữ liệu.
  • [ ] Có thể phân biệt fn (truyền function) và fn() (gọi function ngay) và dự đoán lỗi.
  • [ ] Có thể dự đoán kết quả khi callback không return trong map-like function.
  • [ ] Có thể debug lỗi callback is not a function do truyền kết quả thay vì function.
  • [ ] Có thể giải thích tại sao callback giúp tách biệt logic "lặp" khỏi logic "xử lý".

17. Spiral Connection (Liên kết xoắn ốc) ​

Previous (Trước): Parameters (0.5.4) — bạn đã biết parameter có thể nhận bất kỳ giá trị nào, bao gồm cả function value. Callback là ứng dụng trực tiếp của điều đó.

Current (Hiện tại): Callback — truyền logic vào host function, để host function quyết định khi nào và như thế nào gọi lại.

Next (Tiếp theo):

  • 0.5.6 — Higher-order Functions: callback là một chiều (truyền vào). Higher-order còn bao gồm function trả về function.
  • Stage 1 — Closure: callback được truyền đi vẫn "nhớ" được biến từ scope ngoài. Đây là cơ chế đằng sau mọi callback thực sự hữu ích.
  • Stage 3 — Async & Concurrency: callback bất đồng bộ (setTimeout, Promise, event listener). Host function không gọi callback ngay mà để lại cho event loop.
  • Stage 8 — React: onClick={handleClick} chính là callback pattern. React là host, browser event là trigger.
📴 Offline Mode — Content served from cache