Ở Phần 1, chúng ta đã thấy JavaScript được tải về và thực thi trong trình duyệt - từ cách DNS phân giải tên miền, đến cách rendering pipeline biến HTML/CSS/JS thành pixel, đến cách <script> chặn toàn bộ quá trình đó. Bây giờ, hãy nhìn vào bên trong JavaScript - những khái niệm cốt lõi mà mọi React developer đều phải nắm vững.
Tại sao? Vì React không phát minh ra cú pháp mới. Nó chỉ tận dụng những tính năng sẵn có của JavaScript:
Hiểu JavaScript kỹ hơn thì đọc React sẽ bớt mù mờ hơn. Bắt đầu từ những thứ nhỏ nhất: scope, function, object, array, async.
Hãy bắt đầu từ thứ đơn giản nhất - biến. Biến giống như một chiếc hộp có dán nhãn, bạn bỏ thứ gì vào thì lấy ra thứ đó. JavaScript có ba cách khai báo biến: var, let, và const.
var là kiểu khai báo cũ, có hai đặc tính:
1. Function Scope - nó không bị giới hạn trong block {}, chỉ bị giới hạn trong function. Nghĩa là biến khai báo trong ifvẫn "rò rỉ" ra ngoài!
2. Hoisting- dù bạn khai báo ở dòng 10, JavaScript "kéo" nó lên đầu scope và khởi tạo là undefined.
let và const ra đời từ ES6 để sửa chính vấn đề đó. Chúng có block scope - khai báo trong {} nào thì chỉ tồn tại trong {} đó. Khác biệt: let cho phép gán lại giá trị, const thì không.
Nhưng cẩn thận - const không có nghĩa là bất biến. Nếu const chứa một object, bạn vẫn sửa được property bên trong. const chỉ ngăn bạn gán lại biến sang một giá trị hoàn toàn khác.
| var | let (ES6) | const (ES6) | |
|---|---|---|---|
| Scope | Function Scope: Sống trong toàn bộ hàm chứa nó. | Block Scope: Chỉ sống trong cặp ngoặc {}. | Block Scope: Giống let. |
| Reassign | Được phép gán lại. | Được phép gán lại. | KHÔNG được phép gán lại (nhưng có thể thay đổi nội dung bên trong Object/Array). |
| Hoisting | Được đưa lên đầu scope, khởi tạo là undefined. | Được đưa lên đầu nhưng KHÔNG khởi tạo (rơi vào TDZ). | Giống let (TDZ). |
| Global Object | Tạo thuộc tính trên window (nếu khai báo toàn cục). | Không tạo thuộc tính trên window. | Không tạo thuộc tính trên window. |
const chỉ cấm gán lại biến, chứ không khóa nội dung bên trong object/array. Nghe hơi ngược đời, nên cần nhìn thêm một lớp: JavaScript lưu hai nhóm giá trị theo hai cách khác nhau.
Primitive Types (Nguyên thủy): string, number, bigint, boolean, null, undefined, symbol. Giá trị được lưu trực tiếp trong Stack Memory.
Reference Types (Tham chiếu): object, array, function. Biến chỉ lưu địa chỉ bộ nhớ (reference/pointer) trong Stack, còn giá trị thực sự nằm trong Heap Memory.
Vì vậy, với const user = { name: "An" }, thứ bị cố định là biến user đang trỏ tới object đó. Bạn không được bắt user trỏ sang object khác, nhưng vẫn có thể sửa object mà nó đang trỏ tới.
Đây là điều bạn sẽ thấy liên tục trong React: const [count, setCount] = useState(0). Biến count không được gán lại trực tiếp; khi muốn thay đổi state, bạn gọi setter function setCount. React không phát minh ra pattern này từ hư không - nó tận dụng đúng cách constvà array destructuring hoạt động trong JavaScript.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}Hoisting là thuật ngữ trong JavaScript, đề cập đến quá trình di chuyển khai báo của function, biến hoặc class lên đầu phạm vi của chúng, trước khi mã được thực thi.
Thường mọi người hay được nghe rằng "var có hoisting, còn let/const thì không". Nhưng điều này SAI về mặt kỹ thuật.
Thực tế: let và const CÓ được Hoisting, nhưng cơ chế khởi tạo (initialization) của chúng khác var, và đó chính là nguyên nhân sinh ra Temporal Dead Zone (TDZ).
Với var, bạn có thể log biến trước khi khai báo (ra undefined). Nhưng với let/const, code sẽ crash ngay lập tức:
TDZ (Temporal Dead Zone) là khoảng thời gian từ khi biến được hoisted (đầu block) đến khi nó được khởi tạo (dòng khai báo). Trong khoảng này, truy cập biến sẽ gây lỗi.
// THÍ NGHIỆM: Hãy thử chạy đoạn code dưới đây
// Sau đó thử đổi let → var và chạy lại để thấy sự khác biệt
function testTDZ() {
console.log('Trước khai báo:', typeof myVar); // Thử đoán kết quả?
// Bỏ comment dòng dưới để thấy TDZ crash
// console.log("Trước khai báo:", myLet);
var myVar = 'var hoạt động';
let myLet = 'let hoạt động';
console.log('Sau khai báo:', myVar);
console.log('Sau khai báo:', myLet);
}
testTDZ();Closure xảy ra khi một hàm "nhớ" được các biến từ scope bên ngoài nó, ngay cả khi scope đó đã kết thúc. Hãy tưởng tượng hàm con được bao bọc bởi một "bong bóng nhớ" - bong bóng này giữ lại mọi biến mà hàm con cần, không để chúng biến mất.
Mỗi lần component re-render, useState trả về giá trị state hiện tại nhờ closure- nó "nhớ" state đang được lưu ở đâu trong bộ nhớ nội bộ của React.
Stale Closure Bug: Nếu bạn từng gặp bug trong useEffect- giá trị state cũ bị "đóng băng" trong callback - đó chính là closure đang hoạt động. Hàm callback "nhớ" giá trị state tại thời điểm nó được tạo ra, không phải giá trị mới nhất. Hiểu closure = biết cách fix stale closure bằng dependency array.
function makeGreeting(greeting) {
return function(name) {
console.log(greeting + ', ' + name + '!');
};
}
const sayHello = makeGreeting('Xin chào');
const sayBye = makeGreeting('Tạm biệt');
sayHello('Minh');
sayBye('Lan');Trong phần Variable, chúng ta biết object là Reference Type - biến chỉ lưu địa chỉ (reference), không lưu giá trị thực. Điều này tạo ra một quy tắc vàng trong React mà bạn cần khắc cốt ghi tâm:
Không bao giờ thay đổi trực tiếp object state.
Tại sao? Vì React so sánh state cũ và mới bằng reference - tức là nó kiểm tra "đây có phải cùng một object trong bộ nhớ không?". Nếu bạn sửa trực tiếp property mà không tạo object mới, reference không đổi, React nghĩ không có gì thay đổi, và giao diện... đứng im.
Và đây là lúc spread operator trở thành người hùng:
Ba dấu chấm ... gọi là spread syntax. Nó "trải" tất cả property của object cũ ra, rồi bạn ghi đè property nào cần sửa. Kết quả là một object hoàn toàn mới - khác reference - và React nhận ra thay đổi.
Tương tự với mảng:
Pattern này gọi là immutability - không thay đổi dữ liệu gốc, mà luôn tạo bản mới. Đây không phải khái niệm của React - đó là tư duy lập trình hàm (functional programming) trong JavaScript, và React chỉ đang thúc đẩy bạn viết code theo cách đó.
Spread operator tạo bản sao cấp 1 - nhưng nested objects bên trong vẫn chia sẻ reference:
Fix - spread từng cấp:
Đây là nguồn bug phổ biến nhất khi update nested state. Với state phức tạp nhiều tầng, cân nhắc dùng Immer(tích hợp sẵn trong Redux Toolkit) - cho phép viết code "mutation" mà vẫn tạo object mới bên dưới. Hoặc dùng structuredClone() cho deep copy hoàn toàn.
const [user, setUser] = useState({ name: 'Nam', age: 25 });
function updateAge() {
user.age = 26;
setUser(user);
}Đây là phép so sánh an toàn và nên dùng trong 99% trường hợp. Nó hoạt động đơn giản:
Kiểu dữ liệu có giống nhau không? → Nếu KHÔNG: Trả về false ngay lập tức. Nếu kiểu giống nhau: So sánh giá trị.
Không ép kiểu, không bất ngờ. Rõ ràng, minh bạch.
Ở phép so sánh này, JS sẽ ngầm thực hiện Type Coercion (ép kiểu). Nếu hai vế khác kiểu, JS sẽ cố gắng chuyển đổi chúng về cùng một kiểu (thường là Number) để so sánh.
Một số kết quả gây "lú":
Thử thách: Dựa trên kiến thức ở trên, bạn hãy giải thích tại sao [] == 0 trả về true nhé!
Gợi ý: [] → "" (toString) → 0 (toNumber) → so sánh với 0.
Về bản chất, Object trong JS là một tập hợp các cặp Key-Value không có thứ tự.
Memory: Object là Reference Type, được lưu trong bộ nhớ Heap.
Cấu trúc: Nó giống như một cái Hash Map hoặc Dictionary.
Trong React, state, props, context... tất cả đều là object. Khi bạn viết <Button color="red" size="lg" />, React đóng gói tất cả props thành một object: { color: "red", size: "lg" } và truyền vào component.
Tại sao chúng ta không khai báo hàm toString() mà lại có thể dùng được? → Đó là nhờ Prototype.
JS tạo ra một "kho chung" chứa các hàm dùng chung (đây là Prototype). Các object con chỉ cần giữ một đường link trỏ về kho chung đó để dùng ké.
Phân biệt prototype và __proto__:
1. prototype: Chỉ có ở Function(đặc biệt là Constructor Function). Nó là cái "khuôn mẫu" để quy định: "Những đứa con do tao tạo ra (bằng từ khóa new) sẽ được thừa hưởng cái gì".
2. __proto__: Có ở Mọi Object. Nó là cái đường link bí mật trỏ về cha của nó. Lưu ý: __proto__ là cách viết cũ (nhưng vẫn dùng nhiều để debug). Chuẩn hiện đại là dùng Object.getPrototypeOf(obj).
Mối quan hệ: Con.__proto__ === Cha.prototype
Prototype Chain: Prototype cũng là một object, và nó có thể có prototype riêng của mình, tạo thành một chuỗi.
Khi bạn truy cập nam.toString(), JS sẽ đi đi theo quy trình:
Bước 1: Tìm trong chính object nam. Có hàm toString không? → Không.
Bước 2: Nhìn vào nam.__proto__ (tức là Person.prototype). Có không? → Không.
Bước 3: Nhìn tiếp vào Person.prototype.__proto__ (tức là Object.prototype - Trùm cuối). Có không? → CÓ!
Bước 4: Chạy hàm đó. Nếu đến bước cuối (Object.prototype.__proto__ là null) mà vẫn không tìm thấy → Trả về undefined.
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return '...';
};
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
const rex = new Dog('Rex');Ở phần trên, chúng ta thấy Object có prototype chain- nghĩa là mọi object đều "thừa kế" sẵn các property từ Object.prototype (như toString, constructor...). Đây là tính năng mạnh mẽ, nhưng cũng là một hạn chế khi bạn muốn dùng Object như một "kho chứa" key-value thuần tuý. ES6 mang đến một cấu trúc dữ liệu sinh ra để giải quyết đúng vấn đề đó.
Trước ES6, chúng ta luôn dùng Object để làm Hash Map (Dictionary). Tuy nhiên, Object có 2 hạn chế lớn:
1. Key bắt buộc phải là String (hoặc Symbol): Bạn không thể dùng một Object khác làm Key cho một Object.
2. Rủi ro Prototype: Object thường kế thừa từ Object.prototype, nên nó chứa sẵn các key rác (như toString, constructor...) dễ gây bug nếu bạn không cẩn thận.
Map ra đời để khắc phục hoàn toàn 2 điều này.Nó là một "kho chứa" sạch sẽ, chuyên biệt.
Map là một tập hợp key-value, tương tự như Object, nhưng linh hoạt và nhất quán hơn. Key trong Map có thể là bất kỳ kiểu dữ liệu nào: kiểu nguyên thủy (string, number, boolean, NaN), object, function.
Các phương thức cốt lõi:
- set(key, value) - thêm mới hoặc cập nhật
- get(key) - lấy giá trị
- has(key) - kiểm tra sự tồn tại của khóa
- delete(key) - xóa một mục
- clear() - xóa tất cả
- size - số lượng phần tử
Nghe tới đây thì Map có vẻ như “xịn hơn” Object trên mọi mặt trận. Nhưng thực tế không hẳn vậy: mỗi cấu trúc đều có ngữ cảnh dùng riêng.
Cách dễ nhất để thấy sự khác nhau là đặt chúng cạnh nhau.
| Object | Map | |
|---|---|---|
| Key types | Chỉ là string hoặc symbol (số sẽ bị chuyển thành string). | Bất kỳ giá trị nào (kiểu nguyên thủy, object, function, NaN). |
| Order of keys | Từ ES2015: integer keys xếp trước (tăng dần), sau đó string keys theo thứ tự chèn. | Giữ chặt chẽ thứ tự chèn. |
| Iteration | Cần các hàm hỗ trợ (Object.keys, Object.entries, for...in). | Có sẵn iterable (for...of, forEach, keys, values). |
| Performance | Tối ưu cho tập nhỏ các thuộc tính tĩnh. | Tối ưu cho thêm / xóa / tra cứu thường xuyên. |
| Prototype issues | Có prototype chain. | Kho key-value thuần, không có khóa mặc định. |
| Serialization | Hoạt động trực tiếp với JSON.stringify. | Cần chuyển đổi (ví dụ: Object.fromEntries(map)). |
| Nên dùng | Dữ liệu có cấu trúc / cấu hình tĩnh. | Dữ liệu động, tra cứu nhanh, hoặc khi khóa không phải string. |
Nếu biến là nơi chứa dữ liệu, thì hàm là cỗ máy xử lý dữ liệu đó. Và trong React, hàm đóng vai trò cực kỳ quan trọng - vì mỗi component về bản chất chỉ là một hàm trả về giao diện.
Ba ô trong playground phía trên minh họa 3 điều mà JavaScript cho phép bạn làm với hàm, giống hệt cách bạn dùng một giá trị thông thường như number hay string:
JavaScript có hai cách viết hàm phổ biến:
Arrow function ngắn gọn hơn, nhưng khác biệt thật sự không nằm ở cú pháp - mà nằm ở cách chúng xử lý this (chúng ta sẽ tìm hiểu kỹ ở phần tiếp theo).
Nhưng hàm trong JavaScript còn có một điều đặc biệt - chúng là first-class citizen (đối tượng hạng nhất). Nghĩa là hàm có thể:
- Được gán vào biến
- Được truyền vào hàm khác như tham số (callback)
- Được trả về từ hàm khác
Đây chính là nền tảng của callback - và callback ở khắp nơi trong React:
Mỗi arrow function trong các ví dụ trên đều là callback - một hàm được truyền vào hàm/component khác để chạy sau. Đây là JavaScript thuần, không phải cú pháp riêng của React.
Nguyên tắc duy nhất cần nhớ: Ai gọi hàm, this là người đó. Arrow function là ngoại lệ - nó khóa cứng this tại nơi được định nghĩa.
Đây là lý do React class component phải bind(this) cho event handler. Arrow function và function component giải quyết triệt để vấn đề này.
const obj = {
title: 'React',
getTitle: function() {
return this.title;
},
getTitleArrow: () => this.title,
};
console.log(obj.getTitle());
console.log(obj.getTitleArrow());Ba hàm này cho phép bạn ép this trỏ vào object tuỳ chọn - như "mượn" method của người khác để dùng.
| call | apply | bind | |
|---|---|---|---|
| Thực thi | Ngay lập tức | Ngay lập tức | Trả về hàm mới |
| Tham số | Rời: a, b, c | Mảng: [a, b, c] | Rời (hoặc truyền sau) |
| Dùng khi | Mượn hàm dùng 1 lần | Dữ liệu dạng mảng | Giữ this trong callback |
Trong class component: this.handleClick = this.handleClick.bind(this) - vì khi truyền method vào addEventListener hay JSX onClick, this mất ngữ cảnh. bind khóa cứng this lại. Arrow function và function component khiến pattern này không còn cần thiết.
function greet(greeting) {
return `${greeting}, ${this.name}!`;
}
const user = { name: 'Minh' };
const boundGreet = greet.bind(user, 'Xin chào');
console.log(boundGreet());
console.log(greet.call(user, 'Hello'));Khi bạn có một mảng dữ liệu - danh sách sản phẩm, danh sách user, danh sách bình luận - bạn cần biến chúng thành giao diện. Và trong React, bạn sẽ dùng .map() ở mọi nơi.
.map() nhận vào một callback, chạy callback đó trên từng phần tử, và trả về một mảng mới. Nó không thay đổi mảng gốc.
Arrow function làm callback, trả về JSX. Tất cả JavaScript thuần.
.filter() lọc ra các phần tử thỏa điều kiện, trả về mảng mới. Trong React, bạn dùng nó để hiển thị có điều kiện, hoặc để xóa phần tử khỏi state mà không làm biến đổi state.
.reduce() mạnh mẽ nhất - có thể gom cả mảng thành một giá trị duy nhất: tổng tiền giỏ hàng, nhóm sản phẩm theo danh mục, đếm số lượng.
Điểm chung: Cả ba đều nhận callback, đều trả về giá trị mới, đều không thay đổi mảng gốc. Đây chính là phong cách lập trình hàm mà React yêu thích.
const numbers = [1, 2, 3, 4, 5];
const result = numbers.filter(n => n % 2 === 0).map(n => n * 10);JavaScript là ngôn ngữ Single-threaded (Đơn luồng). Nghĩa là nó chỉ có một Call Stack, chỉ làm được một việc tại một thời điểm. Nó thực thi code theo trình tự từ trên xuống, theo nguyên tắc LIFO (Last In First Out).
Thông qua sơ đồ này ta có thể thấy các hàm và lệnh được đẩy vào ngăn xếp và thực thi theo nguyên tắc LIFO, sau khi chúng hoàn thành sẽ bị loại bỏ khỏi ngăn xếp.
Call Stack bị "khoá"
Có đoạn code gọi API mất 5 giây. Vấn đề:
- Trong 5 giây chờ dữ liệu, Call Stack bị "khoá"
- User click button khác? Không có phản ứng!
- Hover effect trên menu? Không thấy gì!
- Scroll trang? Lag kinh khủng!
Browser bị "Đơ" trong 5 giây - người dùng tức giận rời trang!
Vậy chúng ta cần làm sao? → Sử dụng Asynchronous - đẩy tác vụ tốn thời gian ra ngoài, để Call Stack tiếp tục xử lý việc khác.
Trong React, khi bạn fetch dữ liệu trong useEffect, bạn đang dùng async pattern:
async/await chỉ là cú pháp đẹp hơn để làm việc với Promise - thay vì chuỗi .then().then(), bạn viết code trông giống đồng bộ, nhưng thực chất vẫn bất đồng bộ. Keyword await tạm dừng hàm tại dòng đó cho đến khi Promise resolve, rồi mới chạy tiếp.
JavaScript vẫn là single-threaded- nó không thể "vừa chờ vừa làm việc khác" cùng lúc. Vậy làm sao được? Thông qua "Event Loop"!
Hãy tưởng tượng: JavaScript có một bàn làm việc - gọi là Call Stack. Nó chỉ xử lý một task tại một thời điểm trên bàn này. Khi gặp việc tốn thời gian - như gọi API - nó không ngồi chờ. Nó đẩy việc đó sang cho "đồng nghiệp" - Web APIs (do trình duyệt cung cấp). Khi đồng nghiệp làm xong, kết quả được xếp vào hàng chờ (Queue). Event Loop liên tục kiểm tra: "Bàn làm việc trống chưa?". Nếu trống, nó lấy task từ hàng chờ đưa lên bàn.
Hãy dừng lại 30 giây để đoán kết quả được in ra như nào nhé:
Đó là lý do setTimeout(fn, 0) không chạy ngay - nó phải đợi Call Stack trống.
Hãy thử thực hành với ví dụ phức tạp hơn:
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
setTimeout(() => console.log('4'), 0);
console.log('5');Callback của Promise (.then()) không vào Task Queue - mà vào Microtask Queue, có độ ưu tiên cao hơn. Event Loop sẽ xử lý hết microtask trước khi lấy task từ Task Queue.
Vậy khi có cả hai loại, thứ tự ra sao?
Quy tắc:
1. Chạy hết code synchronous trên Call Stack
2. Khi Stack trống → đọc hết Microtask Queue (Promise.then, queueMicrotask)
3. Khi cả Stack và Microtask Queue trống → đọc Task Queue (setTimeout, setInterval)
Hiểu Event Loop giúp bạn trả lời câu hỏi này: state update trong React cũng là bất đồng bộ - nó được batch lại và xử lý trong chu kỳ render tiếp theo, không phải tức thì. Giống như setTimeout callback phải đợi Stack trống, state update phải đợi chu kỳ render tiếp theo.
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
setTimeout(() => console.log('4'), 0);
Promise.resolve().then(() => console.log('5'));
console.log('6');Một ứng dụng React không phải một file duy nhất - nó là hàng chục, hàng trăm file. Mỗi component một file, mỗi hook một file, mỗi utility một file. Và hệ thống cho phép điều đó chính là ES Modules - import và export.
Lưu ý quan trọng:
Named export: Một file có thể export nhiều thứ. Import phải đúng tên (trong ngoặc {}).
Default export: Một file chỉ có một default export. Import có thể đặt tên tùy ý.
Khi bạn import sai path → lỗi "Module not found". Khi bạn nhầm named/default export → lỗi "... is not exported". Hiểu module giúp debug nhanh hơn. Và tree-shaking (loại bỏ code không dùng khi build) chỉ hoạt động tốt với named export - lý do nhiều thư viện React khuyến khích dùng named export.
export const PI = 3.14;
export function add(a, b) { return a + b; }
export default function multiply(a, b) { return a * b; }Destructuring là cú pháp ES6 cho phép bạn "rút" giá trị từ object hoặc array ra biến riêng chỉ trong một dòng. Đây là pattern bạn sẽ dùng mọi lúc mọi nơi trong React.
Thay vì truy cập từng property một, bạn "mở hộp" cùng lúc:
Với array, bạn destructure theo vị trí thay vì tên:
Dữ liệu thực tế thường có nhiều tầng. Destructuring hỗ trợ đào sâu:
Dùng ... để gom phần còn lại vào một biến:
Pattern mạnh mẽ nhất - destructure ngay tại tham số hàm:
Destructuring là DNA của React. Hầu hết mọi thứ bạn viết trong React đều dùng pattern này:
Nếu bạn không vững destructuring, bạn sẽ không đọc được hầu hết React code ngoài thực tế.
const { a, b: { c } = {} } = { a: 1 };
console.log(a, c);Dữ liệu API ngoài đời thật thường không hoàn hảo: thiếu field, nested object bị null, mảng rỗng. Nếu truy cập sai, app sẽ crash với lỗi kinh điển:Cannot read properties of undefined.
Hai toán tử ?. và ?? giúp bạn viết code null-safe để UI không vỡ.
obj?.prop nghĩa là: nếu obj là null hoặc undefined thì trả về undefined luôn, không throw error.
a ?? b nghĩa là: nếu a là null hoặc undefined thì dùng b. Ngược lại giữ nguyên a.
Khác với ||, toán tử ?? không coi 0, '', false là thiếu dữ liệu.
Trong React, đây là cặp toán tử dùng hàng ngày khi render dữ liệu bất đồng bộ từ API:
Quy tắc thực chiến: với dữ liệu có thể thiếu, hãy ưu tiên ?. để truy cập an toàn và ?? để set fallback đúng nghĩa.
Code luôn có thể fail: API trả về 500, JSON parse lỗi, network mất kết nối. Câu hỏi không phải "có lỗi không?" mà là "bạn xử lý lỗi thế nào?". JavaScript cung cấp try/catch/finally để kiểm soát lỗi thay vì để app crash.
Quy tắc quan trọng:
- try: bọc code có rủi ro
- catch: xử lý lỗi (log, thông báo user, fallback)
- finally: cleanup, luôn chạy dù có lỗi hay không
Đây là phần hay bị bỏ quên nhất. Nhiều developer viết await fetch(...) mà không gồm try/catch - app chạy ngon cho đến khi network lỗi thì crash:
Trong ứng dụng thực tế, bạn thường cần phân biệt loại lỗi để hiển thị thông báo phù hợp cho user:
Error handling trong React component là kỹ năng sống còn cho UX tốt. Không xử lý lỗi = user thấy trang trắng:
Pattern loading / error / data với try/catch/finally là nền tảng của mọi data fetching trong React. Thư viện như React Query, SWR đều tự động hóa pattern này - nhưng bạn cần hiểu cơ chế gốc.
React render UI dựa trên state tại thời điểm hiện tại. Vì vậy bạn cần pattern điều kiện ngắn gọn để quyết định hiển thị gì. Short-circuit logic (&&, ||, ??) chính là công cụ cốt lõi.
Khi điều kiện bên trái là true, JSX bên phải được render. Khi false, React bỏ qua.
Nếu có 2 nhánh rõ ràng (A hoặc B), ternary thường dễ đọc hơn dùng nhiều &&.
Conditional rendering là phần bạn dùng liên tục trong React: loading state, error state, empty state, permission state...
Mục tiêu không chỉ là render đúng, mà còn phải dễ đọc, dễ bảo trì, tránh bug ẩn do coercion của JS.
Từ URL → DNS → Rendering → JavaScript core. Chúng là những phần không thể tách rời. React sẽ dùng lại chúng theo những cách rất cụ thể:
| JavaScript Concept | React sẽ dùng nó cho... |
|---|---|
| const + Block scope | State declarations (useState, useReducer) |
| Closure | Hooks "nhớ" state giữa các render |
| Spread + Immutability | State updates không mutation |
| Reference comparison | Detect thay đổi để re-render |
| First-class functions | Components, callbacks, event handlers |
| this + bind | Class components (legacy), thư viện bên thứ 3 |
| Array methods | Render danh sách, filter/transform data |
| Event Loop + Microtask | Hiểu batching, scheduling trong React |
| ES Modules | Tổ chức code, tree-shaking, code splitting |
| Destructuring | Props, useState, useReducer, custom hooks |
| ?. + ?? | Null-safe data access, fallback UI cho API data |
| Error handling (try/catch) | Loading/error/data pattern, fetch an toàn |
| Short-circuit (&&, ||, ??) | Conditional rendering trong JSX |
JavaScript là nền móng. React chỉ là ngôi nhà xây trên đó.
var vs let/const - khác biệt scope?
var = function scope, let/const = block scope
console.log(a); // undefined ← var được hoisted VÀ khởi tạo = undefined
var a = 5;console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10; // Khoảng cách từ đầu block đến dòng này gọi là TDZ// Mutation: React không nhận ra thay đổi
user.age = 26;
setUser(user); // Cùng reference → không re-render!
// Tạo bản sao mới: React re-render
setUser({ ...user, age: 26 }); // Khác reference → re-render!// Mutation
items.push(newItem);
// Immutability
setItems([...items, newItem]); // Thêm phần tử
setItems(items.filter((i) => i.id !== deleteId)); // Xóa phần tửconst user = {
name: 'Nam',
address: { city: 'Hà Nội', district: 'Cầu Giấy' }
};
// Bug: address vẫn cùng reference!
const newUser = { ...user, name: 'Lan' };
newUser.address.city = 'Sài Gòn';
console.log(user.address.city); // "Sài Gòn" ← BUG! user gốc bị thay đổi!// Deep immutable update
const newUser = {
...user,
address: { ...user.address, city: 'Sài Gòn' }
};
console.log(user.address.city); // "Hà Nội" ← Gốc không bị ảnh hưởng!5 == '5'; // true
// Cơ chế: JS chuyển "5" thành 5. Sau đó so sánh 5 == 5.
'10' == 5; // false
1 == true; // true
// Cơ chế: true chuyển thành 1. So sánh 1 == 1.
0 == false; // true
// Cơ chế: false chuyển thành 0. So sánh 0 == 0.
'1' == true; // true
// Cơ chế phức tạp:
// 1. true -> 1
// 2. "1" (string) vs 1 (number) -> "1" chuyển thành 1
// 3. 1 == 1 -> true'' == false; // true (cả hai ép về 0)
'0' == false; // true ("0" → 0, false → 0)
'' == '0'; // false (cùng kiểu string, so sánh giá trị)const person = { kind: 'human' };
const student = Object.create(person);
student.name = 'Sydexa';
console.log(student.kind); // "human" (lấy từ prototype)
console.log(student.__proto__ === person); // trueconst a = 5;
a.toString(); // "5"const myMap = new Map();
// Hoặc có thể khởi tạo sẵn dữ liệu
const userRoles = new Map([
['admin', 1],
['editor', 2],
]);// Function declaration
function greet(name) {
return `Xin chào ${name}`;
}
// Arrow function (ES6)
const greet = (name) => `Xin chào ${name}`;// onClick nhận callback
<button onClick={() => setCount(count + 1)}>Click</button>
// useEffect nhận callback
useEffect(() => {
fetchData();
}, []);
// .map() nhận callback
{users.map((user) => <UserCard key={user.id} user={user} />)}// Case 1: Gọi trực tiếp → this = window (strict mode: undefined)
function show() {
console.log(this);
}
show(); // Window
// Case 2: Gọi từ object → this = object trước dấu chấm
const user = {
name: 'Nam',
sayHi() {
console.log(this.name);
},
};
user.sayHi(); // "Nam"
const f = user.sayHi;
f(); // undefined, mất ngữ cảnh!
// Case 3: Arrow function → this kế thừa từ nơi ĐỊNH NGHĨA
function Person(age) {
this.age = age;
setTimeout(() => console.log(this.age), 100); // 20
setTimeout(function () {
console.log(this.age);
}, 200); // undefined
}
new Person(20);const dev = { name: 'Chiến' };
const other = { name: 'Phương' };
function code(lang) {
console.log(`${this.name} code ${lang}`);
}
code.call(other, 'React'); // "Phương code React", chạy ngay, tham số rời
code.apply(other, ['React']); // "Phương code React", chạy ngay, tham số mảng
const codeLater = code.bind(other, 'React');
codeLater(); // "Phương code React", tạo hàm mới, chạy sauconst fruits = ['Táo', 'Cam', 'Xoài'];
return fruits.map((fruit) => <li key={fruit}>{fruit}</li>);// Xóa user khỏi state (immutable)
setUsers(users.filter((user) => user.id !== deleteId));
// Chỉ hiển thị user active
{users.filter((u) => u.active).map((u) => <UserCard key={u.id} user={u} />)}const total = cart.reduce((sum, item) => sum + item.price, 0);// User click button -> fetch dữ liệu -> update UI
// Giả sử fetchSync là hàm đồng bộ
button.addEventListener('click', () => {
const response = fetchSync('https://api.example.com/users');
// Chờ... 2 giây
// Chờ... 3 giây
// Chờ... đến 5 giây mới có dữ liệu!
updateUI(response);
});// Async version
button.addEventListener('click', async () => {
const response = await fetch('https://api.example.com/users');
// Trong 5 giây này, Call Stack KHÔNG bị khoá
// User vẫn có thể click, scroll, hover -> UI phản ứng bình thường!
updateUI(response);
});useEffect(() => {
const fetchData = async () => {
const res = await fetch('/api/users');
const data = await res.json();
setUsers(data); // Cập nhật state sau khi có dữ liệu
};
fetchData();
}, []);function main() {
console.log('A');
setTimeout(() => {
console.log('B');
}, 0);
console.log('C');
}
main();function main() {
console.log('A');
setTimeout(() => {
console.log('B');
}, 100);
console.log('C');
}
setTimeout(() => {
console.log('D');
}, 0);
main();function main() {
console.log('A');
Promise.resolve().then(() => console.log('B'));
console.log('C');
}
main();
// Kết quả: A, C, Bfunction main() {
console.log('A');
setTimeout(() => {
console.log('B');
}, 0);
Promise.resolve().then(() => console.log('D'));
console.log('C');
}
main();// Button.jsx: export component
export default function Button({ label }) {
return <button>{label}</button>;
}// App.jsx: import component
import Button from './Button';export function formatDate(date) { /* ... */ } // Named export
export function formatMoney(amount) { /* ... */ } // Named exportimport { formatDate, formatMoney } from './utils'; // Named import: phải đúng tênexport default function Button() { /* ... */ } // Default export: mỗi file chỉ 1 cáiimport Button from './Button'; // Có thể đặt tên bất kỳ
import MyButton from './Button'; // Cũng hợp lệ!// Cách cũ - verbose
const name = user.name;
const email = user.email;
const role = user.role;
// Destructuring - gọn gàng
const { name, email, role } = user;
// Đổi tên biến (rename)
const { name: userName, email: userEmail } = user;
// Giá trị mặc định (default value)
const { name, avatar = '/default-avatar.png' } = user;
// Nếu user.avatar là undefined → dùng defaultconst colors = ['red', 'green', 'blue'];
// Destructure theo vị trí
const [first, second, third] = colors;
// first = 'red', second = 'green', third = 'blue'
// Bỏ qua phần tử
const [primary, , accent] = colors;
// primary = 'red', accent = 'blue'
// Giá trị mặc định
const [a, b, c, d = 'white'] = colors;
// d = 'white' (vì colors[3] là undefined)const response = {
data: {
user: {
name: 'Nam',
address: { city: 'HCM', district: '1' }
}
},
status: 200
};
// Nested destructuring
const {
data: {
user: {
name,
address: { city }
}
}
} = response;
console.log(name); // 'Nam'
console.log(city); // 'HCM'const { id, ...restProps } = { id: 1, name: 'Nam', age: 25, role: 'dev' };
// id = 1
// restProps = { name: 'Nam', age: 25, role: 'dev' }
const [head, ...tail] = [1, 2, 3, 4, 5];
// head = 1
// tail = [2, 3, 4, 5]// Thay vì nhận cả object rồi truy cập từng property
function greet(user) {
return `Hello ${user.name}, age ${user.age}`;
}
// Destructure ngay tại parameter
function greet({ name, age = 0 }) {
return `Hello ${name}, age ${age}`;
}
greet({ name: 'Nam', age: 25 }); // "Hello Nam, age 25"
greet({ name: 'An' }); // "Hello An, age 0" (default)// 1. useState - array destructuring
const [count, setCount] = useState(0);
const [user, setUser] = useState(null);
// 2. Props - object destructuring tại parameter
function UserCard({ name, email, avatar = '/default.png' }) {
return (
<div>
<img src={avatar} alt={name} />
<h2>{name}
const user = null;
// Crash
// const city = user.profile.address.city;
// Safe
const city = user?.profile?.address?.city;
console.log(city); // undefined
const apiResponse = {
data: {
user: { name: 'Nam' }
}
};
console.log(apiResponse?.data?.user?.name); // 'Nam'
console.log(apiResponse?.data?.user?.email); // undefinedconst users = [{ name: 'A' }, { name: 'B' }];
console.log(users?.[0]?.name); // 'A'
console.log(users?.[10]?.name); // undefined
const callbacks = {
onSuccess: (msg) => console.log(msg),
};
callbacks.onSuccess?.('Done!'); // chạy
callbacks.onError?.('Failed'); // không crash, bỏ quatry {
// Code có thể throw error
const data = JSON.parse(invalidJson);
console.log(data);
} catch (error) {
// Chạy khi có lỗi trong try block
console.error('Parse failed:', error.message);
} finally {
// LUÔN chạy, dù có lỗi hay không
// Dùng để cleanup: đóng connection, reset loading state
console.log('Done parsing attempt');
}// THIẾU error handling - app crash khi API lỗi
async function getUsers() {
const res = await fetch('/api/users');
const data = await res.json();
return data;
}
// Error handling đầy đủ
async function getUsers() {
try {
const res = await fetch('/api/users');
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
return data;
} catch (error) {
// Network error, JSON parse error, hoặc HTTP error
console.error('Fetch failed:', error.message);
return []; // Fallback value
}
}// Pattern: trả về { data, error } thay vì throw
async function fetchUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
return {
data: null,
error: { type: 'HTTP_ERROR', status: res.status,
message: `Server trả về ${res.status}`
// Hai cách tương đương:
// Cách 1: .catch() - chain style
fetch('/api/data')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error('Lỗi:', err));
// Cách 2: try/catch - async/await style
try {
const res = await fetch('/api/data');
const data = await res.json();
console.log(data);
} catch (err) {
console.error('Lỗi:', err);
}
// CHÚ Ý: fetch() chỉ throw khi NETWORK error
// HTTP 404, 500 KHÔNG throw → phải kiểm tra res.ok// &&: trả về operand đầu tiên nếu falsy, ngược lại trả operand cuối
true && 'A'; // 'A'
false && 'A'; // false
0 && 'A'; // 0
// ||: trả về operand đầu tiên nếu truthy, ngược lại trả operand cuối
'user' || 'Guest'; // 'user'
'' || 'Guest'; // 'Guest'
// ??: chỉ fallback khi null/undefined
0 ?? 100; // 0
null ?? 100; // 100function Notifications({ unreadCount }) {
return (
<div>
<h2>Inbox</h2>
{unreadCount > 0 && <span>Bạn có {unreadCount} tin chưa đọc</span>}
</div>
);
}function TodoList({ todos }) {
return (
<div>
{/* Nếu todos.length = 0, React sẽ render số 0 */}
{todos.length && <p>Có {todos.length} việc cần làm</p>}
{/* Viết an toàn hơn */}
{todos.length > 0 && <p>Có {todos.length} việc cần làm</p>}
</div>
);
}function AuthButton({ isLoggedIn }) {
return (
<div>
{isLoggedIn ? <button>Logout</button> : <button>Login</button>}
</div>
);
}function ProductPrice({ product }) {
return (
<p>
Giá: {product?.price ?? 'Đang cập nhật'}
</p>
);
}
// price = 0 => hiển thị 0 (hợp lệ)
// price = null/undefined => hiển thị 'Đang cập nhật'// Khó đọc và khó debug
{
isAdmin && hasPermission && !isBlocked && items?.length > 0 && (
<Dashboard items={items} />
)
}
// Tách biến trung gian
const canShowDashboard =
isAdmin && hasPermission && !isBlocked && items?.length > 0;
return <>{canShowDashboard && <Dashboard items={items} />}</>;function UserPanel({ users, loading, error }) {
if (loading) return <p>Đang tải...</p>;
if (error) return <p>Lỗi: {error}</p>;
return (
<section>
{users.length === 0 ? (
<p>Chưa có dữ liệu</p>
) : (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name ?? 'Unknown user'}</li>
))}
</ul>
)}
</section>
);
}function Person(name) {
this.name = name;
}
// Thêm hàm vào kho chung (Prototype)
Person.prototype.sayHello = function () {
return 'Hello!';
};
const nam = new Person('Nam');
console.log(nam.name); // "Nam" (Có sẵn trong bản thân nó)
console.log(nam.sayHello()); // "Hello!" (Nó không có, nó mượn của cha)
console.log(nam.__proto__ === Person.prototype); // true// || sẽ fallback khi gặp mọi falsy value
console.log(0 || 100); // 100 (thường là BUG)
console.log('' || 'N/A'); // 'N/A' (thường là BUG)
console.log(false || true); // true (thường là BUG)
// ?? chỉ fallback khi null/undefined
console.log(0 ?? 100); // 0
console.log('' ?? 'N/A'); // ''
console.log(false ?? true); // false
console.log(null ?? 'N/A'); // 'N/A'
console.log(undefined ?? 'N/A'); // 'N/A'const user = {
profile: {
displayName: '',
stats: { followers: 0 }
}
};
// null-safe + giữ đúng 0 và ''
const displayName = user?.profile?.displayName ?? 'Anonymous';
const followers = user?.profile?.stats?.followers ?? 0;
console.log(displayName); // '' (không bị thay bằng 'Anonymous')
console.log(followers); // 0function UserProfile({ data }) {
return (
<section>
<h2>{data?.user?.name ?? 'Anonymous'}</h2>
<p>Email: {data?.user?.email ?? 'Chưa có email'}</p>
<p>Followers: {data?.metrics?.followers ?? 0}</p>
<img
src={data?.user?.avatarUrl ?? '/default-avatar.png'}
alt={data?.user?.name ?? 'Avatar'}
/>
</section>
);
}
// Khi API chưa trả về data:
// - Không crash
// - UI vẫn render fallback hợp lýfunction UserList() {
const [users, setUsers] = useState([]);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false; // Tránh race condition
async function loadUsers() {