SydexaSydexa

React Nâng Cao

Kiến thức cơ bảnJavaScript cho React
TypeScript Cơ BảnType System & GenericUtility Types & tsconfig
ReactJS Căn Bản
Reconciliation & FiberFiber Deep Dive
React HooksOptimization & Utility
React Hook Form & ZodAdvanced Form Patterns
Web Vitals & DevToolsPerformance Playbooks
TanStack VirtualTanStack Virtual (P2)
TanStack QueryTanStack Query (P2)
ZustandZustand (P2)Fiber ↔ External Store
Bảo mật frontendBảo mật frontend (2)
Trang chủKhoá họcReact Nâng Cao
JavaScript Patterns cho React
Objects, prototypes, spread/immutability, array methods, async/await, event loop và ES modules. Những pattern JavaScript mà React dùng hàng ngày.

JavaScript Căn Bản - Nền Móng của React

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

const + destructuring
useState, useReducer
Arrow function
Component, event handler
Closure
Hooks
Spread operator
Immutable state update
.map(), .filter()
Render danh sách
async/await + Promise
Data fetching trong useEffect

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.

Variable - "Hộp chứa đồ" của JavaScript

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.

Variable Scope Explorer

var{ }42Function Scopelet{ }"hi"Block ScopeReferenceError!const{ }{ }= 99 ✗.a=1 ✓Block Scope (locked)Hoisting TimelineScope startDòng khai báoScope endvarundefinedinitializedletTDZinitializedconstTDZinitialized

Bấm trực tiếp vào hộp var, let, const để xem hành vi scope và hoisting

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.

varlet (ES6)const (ES6)
ScopeFunction 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 ObjectTạ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.

Vì sao const không làm object bất biến?

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.

Quick Quiz

1.Đoạn code sau in ra gì?
123
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
1/1
1/1

Temporal Dead Zone (TDZ) - Vùng chết của biến

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 TDZ
JavaScript
1234567891011121314151617
// 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();
⌘+Enter

Quick Quiz

1.let và const có được hoisting không?
1/1
1/1

Closure - "Bong bóng nhớ" của hàm

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.

0:00/0:00
Closure là trái tim của Hooks:

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.

Quick Quiz

1.Đoạn code sau in ra gì?
123456789
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');
1/1
1/1

Spread Operator & Immutability - Tại sao React "ghét" mutation

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.

Mutation vs Immutability

Mutation0x001name: 'Minh'age: 25age: 26 ← sửa tại chỗuser.age = 26; setUser(user)Reference vẫn 0x001!0x001 === 0x001 → Cùng ref!Không thay đổi → bỏ qua!Không re-renderImmutability0x001name: 'Minh'age: 25{ ...user, age: 26 }▼0x002NEWname: 'Minh'age: 260x001 !== 0x002 → Khác ref!Có thay đổi → cập nhật UI!Re-render!

So sánh cùng một state update theo 2 cách: Mutation vs Immutability

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:

Spread Syntax Visual

0x001name: 'Minh'age: 25...user▼name: 'Minh'age: 25age: 26▼ gom lại0x002NEWname: 'Minh'age: 26Object mới, khác reference!

Source object ban đầu: user (0x001)

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 đó.

Cẩn thận: Spread chỉ copy 1 tầng (Shallow Copy)

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.

Quick Quiz

1.Đoạn code React sau có bug. Bug là gì?
123456
const [user, setUser] = useState({ name: 'Nam', age: 25 });

function updateAge() {
  user.age = 26;
  setUser(user);
}
1/1
1/1

Toán tử so sánh == và ===

=== (Strict Equality)

Đâ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.

== (Loose Equality)

Ở 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.

Quick Quiz

1.Kết quả của các phép so sánh sau? null == undefined; // ? null === undefined; // ? NaN == NaN; // ?
1/1
1/1

Object và Prototypes

Object

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.

Prototypes

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é.

Prototype Chain Lookup

Object.prototypetoString()valueOf()hasOwnProperty()__proto__Person.prototypesayHello()__proto__namname: 'Nam'toString()?Không cóKhông cóTÌM THẤY!

Object ban đầu chỉ có name: 'Nam'.

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.

Quick Quiz

1.Khi gọi rex.speak(), JS sẽ tìm ở đâu và theo thứ tự nào?
1234567891011
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');
1/1
1/1

Map

Ở 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.

ObjectMap
Key typesChỉ 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 keysTừ 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.
IterationCần các hàm hỗ trợ (Object.keys, Object.entries, for...in).Có sẵn iterable (for...of, forEach, keys, values).
PerformanceTố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 issuesCó prototype chain.Kho key-value thuần, không có khóa mặc định.
SerializationHoạt động trực tiếp với JSON.stringify.Cần chuyển đổi (ví dụ: Object.fromEntries(map)).
Nên dùngDữ 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.

Quick Quiz

1.Bạn cần lưu trữ mapping giữa các DOM element và dữ liệu liên quan. Nên dùng Object hay Map?
1/1
1/1

Hàm & First-Class Functions - Linh hồn của React

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.

First-Class Functions Playground

ƒ() { ... }

Click vào từng ô bên dưới để đặt hàm vào đó (0/3)

① Gán vào biến
const greet = ___
ƒ() { ... }
const greet = function() {...}
✓ Gán vào biến
② Truyền làm tham số
setTimeout( ___ , 1000)
ƒ() { ... }
setTimeout(greet, 1000)
✓ Tham số (callback)
③ Trả về từ hàm
function outer() {
  return ___
}
ƒ() { ... }
return function() {...}
✓ Trả về từ hàm
First-Class Citizen— hàm = dữ liệu

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:

  • Gán vào biến - const greet = function() {...}: hàm được lưu trong biến, biến greet giờ chính là hàm đó. Gọi greet() sẽ chạy hàm.
  • Truyền làm tham số (callback) - setTimeout(greet, 1000): truyền hàm vào hàm khác như một đối số. Hàm được truyền vào gọi là callback. React dùng pattern này ở khắp nơi: onClick, useEffect, .map().
  • Trả về từ hàm - return function() {...}: một hàm có thể tạo ra và trả về hàm mới. Đây là nền tảng của closure và higher-order functions.

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.

Quick Quiz

1.Trong React, tại sao chúng ta viết onClick={() => handleClick()} thay vì onClick={handleClick()}?
1/1
1/1

this trong JavaScript

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.

0:00/0:00

Đâ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.

Quick Quiz

1.Đoạn code sau in ra gì?
12345678910
const obj = {
  title: 'React',
  getTitle: function() {
    return this.title;
  },
  getTitleArrow: () => this.title,
};

console.log(obj.getTitle());
console.log(obj.getTitleArrow());
1/1
1/1

Call, Apply, Bind

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.

callapplybind
Thực thiNgay lập tứcNgay lập tứcTrả về hàm mới
Tham sốRời: a, b, cMảng: [a, b, c]Rời (hoặc truyền sau)
Dùng khiMượn hàm dùng 1 lầnDữ liệu dạng mảngGiữ 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.

Quick Quiz

1.Kết quả của đoạn code sau?
1234567
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'));
1/1
1/1

Array Methods - Bộ công cụ render danh sách

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.

Array Methods Transformer

fruits"Táo""Cam""Xoài""Chuối"ƒ(x)"Táo""Cam""Xoài""Chuối"count0

Chọn một method để xem cách nó biến đổi mảng

.map() - Biến đổi từng phần tử

.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 phần tử theo điều kiệ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() - Gom mảng thành một giá trị

.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.

Quick Quiz

1.Kết quả của đoạn code dưới đây là gì?
12
const numbers = [1, 2, 3, 4, 5];
const result = numbers.filter(n => n % 2 === 0).map(n => n * 10);
1/1
1/1

Asynchronous JavaScript

Vấn đề: Synchronous

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!

Giải pháp: Asynchronous

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.

Quick Quiz

1.Tại sao JavaScript cần cơ chế bất đồng bộ (asynchronous)?
1/1
1/1

Event Loop - "Nhạc trưởng" điều phối JavaScript

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"!

Interactive Event Loop

0/6
Codeconsole.log('A');setTimeout(() => { console.log('B');}, 0);console.log('C');Call StackWeb APIsConsoleMicrotask QueueTask Queueasyncdone↻Event Loop

Bấm Next Step để bắt đầu thực thi code

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.

Macrotasks (Task Queue)

Hãy dừng lại 30 giây để đoán kết quả được in ra như nào nhé:

Event Loop: Macrotask Flow

CALL STACKWEB APISCONSOLETASK QUEUEmain()callbackTimer 0mscallback> A> C> BEvent Loop

Code chuẩn bị chạy: console.log("A"), setTimeout(...), console.log("C").

Đó 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:

Quick Quiz

1.Kết quả in ra của đoạn code dưới đây là gì;
12345
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
setTimeout(() => console.log('4'), 0);
console.log('5');
1/1
1/1

Microtasks

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.

Event Loop: Microtask Queue Priority

CALL STACKCONSOLEMICROTASK QUEUETASK QUEUE(trống)main() callback> A> C> B Event Loop

Code chuẩn bị chạy: console.log("A"), Promise.then(...), console.log("C").

Mix: Macrotask + Microtask

Vậy khi có cả hai loại, thứ tự ra sao?

Event Loop: Macrotask + Microtask Combined

CALL STACKWEB APISCONSOLEMICROTASK QUEUETASK QUEUEmain()  callbackcallback> A> C> D> B Event Loop

Code chuẩn bị chạy: sync A/C, Promise microtask D, setTimeout macrotask B.

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)

Tại sao console.log(state) ngay sau setState lại ra giá trị cũ?:

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.

Quick Quiz

1.Kết quả in ra của đoạn code dưới đây là gì?
123456
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');
1/1
1/1

ES Modules - Cách tổ chức code trong React

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.

Named Export vs Default 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.

Quick Quiz

1.Đoạn import nào sau đây SAI cú pháp?
123
export const PI = 3.14;
export function add(a, b) { return a + b; }
export default function multiply(a, b) { return a * b; }
1/1
1/1

Destructuring - Mở hộp dữ liệu gọn gàng

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.

Object Destructuring

Thay vì truy cập từng property một, bạn "mở hộp" cùng lúc:

Array Destructuring

Với array, bạn destructure theo vị trí thay vì tên:

Nested Destructuring

Dữ liệu thực tế thường có nhiều tầng. Destructuring hỗ trợ đào sâu:

Rest Pattern (...rest)

Dùng ... để gom phần còn lại vào một biến:

Destructuring trong Function Parameters

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

Quick Quiz

1.Đoạn code sau in ra gì?
12
const { a, b: { c } = {} } = { a: 1 };
console.log(a, c);
1/2
1/2

Optional Chaining (?.) và Nullish Coalescing (??)

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ỡ.

Optional Chaining (?.)

obj?.prop nghĩa là: nếu obj là null hoặc undefined thì trả về undefined luôn, không throw error.

Optional Chaining với array và function

Nullish Coalescing (??)

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.

Kết hợp ?. và ??

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.

Quick Quiz

1.Biểu thức nào là lựa chọn đúng để hiển thị số lượt like, trong đó 0 là giá trị hợp lệ?
1/2
1/2

Error Handling - Xử lý lỗi đúng cách

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.

try/catch/finally cơ bản

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

Error Handling với Async/Await

Đâ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:

Structured Error Shape (Cấu trúc định dạng lỗi)

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:

Promise .catch() vs try/catch

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.

Quick Quiz

1.fetch() trả về rejected Promise trong trường hợp nào?
1/2
1/2

Short-circuit Logic cho Conditional Rendering

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.

Ôn nhanh cơ chế short-circuit

Pattern 1: && để render có điều kiện

Khi điều kiện bên trái là true, JSX bên phải được render. Khi false, React bỏ qua.

Cạm bẫy phổ biến: 0 render ra màn hình

Pattern 2: Ternary để chọn giữa 2 nhánh UI

Nếu có 2 nhánh rõ ràng (A hoặc B), ternary thường dễ đọc hơn dùng nhiều &&.

Pattern 3: ?? cho fallback text/value an toàn

Tránh lạm dụng logic phức tạp trong JSX

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.

Quick Quiz

1.Đoạn JSX nào có thể gây render ký tự "0" ngoài ý muốn?
1/2
1/2

Nhìn lại bức tranh toàn cảnh

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 ConceptReact sẽ dùng nó cho...
const + Block scopeState declarations (useState, useReducer)
ClosureHooks "nhớ" state giữa các render
Spread + ImmutabilityState updates không mutation
Reference comparisonDetect thay đổi để re-render
First-class functionsComponents, callbacks, event handlers
this + bindClass components (legacy), thư viện bên thứ 3
Array methodsRender danh sách, filter/transform data
Event Loop + MicrotaskHiểu batching, scheduling trong React
ES ModulesTổ chức code, tree-shaking, code splitting
DestructuringProps, 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?

Nhấp vào thẻ để lật

var = function scope, let/const = block scope

Nhấp vào thẻ để lật lại
1/15
Bài trướcBài tiếp theo
js
12
console.log(a); // undefined ← var được hoisted VÀ khởi tạo = undefined
var a = 5;
js
12
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
js
123456
// 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!
js
123456
// 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ử
js
12345678910
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!
js
1234567
// 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!
js
123456789101112131415
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
js
123
'' == 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ị)
js
123456
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);   // true
js
123
const a = 5;

a.toString(); // "5"
js
123456
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],
]);
js
1234567
// Function declaration
function greet(name) {
  return `Xin chào ${name}`;
}

// Arrow function (ES6)
const greet = (name) => `Xin chào ${name}`;
jsx
12345678910
// 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} />)}
js
1234567891011121314151617181920212223242526
// 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);
js
123456789101112
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 sau
jsx
12
const fruits = ['Táo', 'Cam', 'Xoài'];
return fruits.map((fruit) => <li key={fruit}>{fruit}</li>);
jsx
12345
// 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} />)}
js
1
const total = cart.reduce((sum, item) => sum + item.price, 0);
js
12345678910
// 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);
});
js
12345678
// 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);
});
js
12345678
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();
}, []);
js
123456789
function main() {
  console.log('A');
  setTimeout(() => {
    console.log('B');
  }, 0);
  console.log('C');
}

main();
js
123456789101112131415
function main() {
  console.log('A');

  setTimeout(() => {
    console.log('B');
  }, 100);

  console.log('C');
}

setTimeout(() => {
  console.log('D');
}, 0);

main();
js
12345678910
function main() {
  console.log('A');

  Promise.resolve().then(() => console.log('B'));

  console.log('C');
}

main();
// Kết quả: A, C, B
js
123456789101112
function main() {
  console.log('A');

  setTimeout(() => {
    console.log('B');
  }, 0);

  Promise.resolve().then(() => console.log('D'));
  console.log('C');
}

main();
Button.jsx
jsx
1234
// Button.jsx: export component
export default function Button({ label }) {
  return <button>{label}</button>;
}
App.jsx
jsx
12
// App.jsx: import component
import Button from './Button';
utils.js
js
12
export function formatDate(date) { /* ... */ }   // Named export
export function formatMoney(amount) { /* ... */ } // Named export
App.jsx
jsx
1
import { formatDate, formatMoney } from './utils'; // Named import: phải đúng tên
Button.jsx
jsx
1
export default function Button() { /* ... */ } // Default export: mỗi file chỉ 1 cái
App.jsx
jsx
12
import Button from './Button';     // Có thể đặt tên bất kỳ
import MyButton from './Button';   // Cũng hợp lệ!
js
1234567891011121314
// 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 default
js
12345678910111213
const 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)
js
12345678910111213141516171819202122
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'
js
1234567
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]
js
123456789101112
// 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)
jsx
1234567891011121314151617181920212223242526272829303132
// 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}
js
1234567891011121314151617
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); // undefined
js
1234567891011
const 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ỏ qua
js
123456789101112
try {
  // 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');
}
js
123456789101112131415161718192021222324
// 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
  }
}
js
1234567891011121314151617181920212223242526272829303132
// 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}`
js
12345678910111213141516171819
// 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
js
123456789101112
// &&: 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;       // 100
jsx
12345678
function Notifications({ unreadCount }) {
  return (
    <div>
      <h2>Inbox</h2>
      {unreadCount > 0 && <span>Bạn có {unreadCount} tin chưa đọc</span>}
    </div>
  );
}
jsx
1234567891011
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>
  );
}
jsx
1234567
function AuthButton({ isLoggedIn }) {
  return (
    <div>
      {isLoggedIn ? <button>Logout</button> : <button>Login</button>}
    </div>
  );
}
jsx
12345678910
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'
jsx
123456789101112
// 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} />}</>;
jsx
123456789101112131415161718
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>
  );
}
js
1234567891011
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
js
1234567891011
// || 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'
js
12345678910111213
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);   // 0
jsx
1234567891011121314151617
function 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ý
jsx
12345678910111213141516171819202122232425262728293031323334353637
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() {

</h2>
<p>{email}</p>
</div>
);
}
// 3. Rest props - forward props xuống child
function Button({ variant, children, ...rest }) {
return (
<button className={`btn-${variant}`} {...rest}>
{children}
</button>
);
}
// <Button variant="primary" onClick={handleClick} disabled>
// → rest = { onClick: handleClick, disabled: true }
// 4. useReducer - array destructuring
const [state, dispatch] = useReducer(reducer, initialState);
// 5. Custom hooks - trả về tuple hoặc object
const { data, error, isLoading } = useFetch('/api/users');
const [value, setValue] = useLocalStorage('theme', 'dark');
}
};
}
const data = await res.json();
return { data, error: null };
} catch (error) {
return {
data: null,
error: { type: 'NETWORK_ERROR',
message: 'Không thể kết nối đến server' }
};
}
}
// Sử dụng
const { data, error } = await fetchUser(123);
if (error) {
if (error.type === 'NETWORK_ERROR') showOfflineMessage();
if (error.status === 404) showNotFound();
return;
}
// data sẵn sàng sử dụng an toàn
try
{
setLoading(true);
setError(null);
const res = await fetch('/api/users');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (!cancelled) setUsers(data);
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
}
loadUsers();
return () => { cancelled = true; }; // Cleanup
}, []);
if (loading) return <p>Đang tải...</p>;
if (error) return <p>Lỗi: {error}</p>;
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}