Mục tiêu: Nắm chắc các core concepts của React thông qua lý thuyết ngắn gọn + ví dụ thực tế + project tập code.
Yêu cầu đầu vào: Biết HTML, CSS, JavaScript cơ bản (ES6+).
- Functional Component
- JSX
- Props vs State
- useState
- useEffect
- Re-render Mechanism
- Component Lifecycle
- Virtual DOM
- Bonus: useRef, useCallback, useMemo
- 🚀 Project Tập Code
Functional Component là một hàm JavaScript trả về JSX. Đây là cách viết component hiện đại nhất trong React (thay thế Class Component từ React 16.8+).
// ✅ Functional Component — đơn giản, rõ ràng
function Greeting({ name }) {
return <h1>Xin chào, {name}!</h1>;
}
// Arrow function cũng OK
const Greeting = ({ name }) => <h1>Xin chào, {name}!</h1>;- Tên component phải viết hoa chữ cái đầu (PascalCase):
UserCard,LoginForm - Tên file nên trùng với tên component:
UserCard.jsx
Functional Component = hàm nhận vào props → trả ra UI (JSX). Không có gì phức tạp hơn thế.
JSX (JavaScript XML) là cú pháp cho phép viết HTML-like code bên trong JavaScript. Trình biên dịch (Babel) sẽ chuyển JSX thành React.createElement(...).
// JSX
const element = <h1 className="title">Hello World</h1>;
// Babel biên dịch thành:
const element = React.createElement("h1", { className: "title" }, "Hello World");| Quy tắc | Sai ❌ | Đúng ✅ |
|---|---|---|
| Chỉ return 1 root element | return <h1/><p/> |
return <><h1/><p/></> |
Dùng className thay class |
class="btn" |
className="btn" |
Biểu thức JS dùng {} |
<p>name</p> |
<p>{name}</p> |
| Self-closing tag | <input> |
<input /> |
| Style dùng object | style="color:red" |
style={{ color: 'red' }} |
function ProductCard({ product }) {
return (
// Fragment <> thay cho div không cần thiết
<>
<h2 className="product-name">{product.name}</h2>
<p style={{ color: product.inStock ? 'green' : 'red' }}>
{product.inStock ? 'Còn hàng' : 'Hết hàng'}
</p>
{/* Render có điều kiện */}
{product.discount > 0 && <span>-{product.discount}%</span>}
</>
);
}| Props | State | |
|---|---|---|
| Định nghĩa | Dữ liệu truyền từ ngoài vào component | Dữ liệu nội bộ của component |
| Ai quản lý | Component cha | Chính component đó |
| Có thay đổi được không? | ❌ Không (read-only) | ✅ Có (dùng setter) |
| Thay đổi có gây re-render? | ✅ Có | ✅ Có |
// Props — cha truyền xuống, con KHÔNG được sửa
function Button({ label, onClick, disabled }) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
}
// State — component tự quản lý
function Counter() {
const [count, setCount] = useState(0); // state nội bộ
return (
<div>
<p>Đếm: {count}</p>
<Button label="Tăng" onClick={() => setCount(count + 1)} />
</div>
);
}"Props flows down, events bubble up" — Dữ liệu đi từ cha → con qua props. Con muốn báo cha thì gọi callback function được truyền qua props.
useState là hook dùng để lưu trữ và cập nhật dữ liệu trong component. Mỗi lần state thay đổi, component sẽ re-render.
const [state, setState] = useState(initialValue);
// ↑ ↑ ↑
// giá trị hàm cập nhật giá trị ban đầufunction LoginForm() {
// 1. State đơn giản
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
// 2. State là object
const [form, setForm] = useState({ username: '', password: '' });
// 3. Updater function (dùng khi state mới phụ thuộc state cũ)
const [count, setCount] = useState(0);
const increment = () => setCount(prev => prev + 1); // ✅ Luôn dùng cách này khi cần prev
// 4. Cập nhật object state — PHẢI spread để giữ các field khác
const handleChange = (field) => (e) => {
setForm(prev => ({ ...prev, [field]: e.target.value })); // ✅
// setForm({ [field]: e.target.value }); ❌ Sẽ mất các field khác!
};
return (
<form>
<input
value={form.username}
onChange={handleChange('username')}
placeholder="Username"
/>
<input
type="password"
value={form.password}
onChange={handleChange('password')}
placeholder="Password"
/>
</form>
);
}// ❌ Sai: setState không cập nhật ngay lập tức
const handleClick = () => {
setCount(count + 1);
console.log(count); // Vẫn là giá trị CŨ!
};
// ❌ Sai: Mutate state trực tiếp
const addItem = () => {
items.push(newItem); // KHÔNG BAO GIỜ làm vậy!
setItems(items);
};
// ✅ Đúng: Tạo array mới
const addItem = () => {
setItems(prev => [...prev, newItem]);
};useEffect dùng để thực hiện side effects — những việc xảy ra ngoài luồng render như: gọi API, đăng ký event listener, set timeout, thao tác DOM trực tiếp.
useEffect(() => {
// Code chạy sau mỗi render có dependency thay đổi
return () => {
// Cleanup — chạy trước khi effect chạy lại hoặc component unmount
};
}, [dependency1, dependency2]); // Dependency array// 1. Chạy sau MỖI lần render (hiếm dùng, cẩn thận infinite loop)
useEffect(() => {
console.log('Component đã render');
});
// 2. Chạy MỘT LẦN khi mount (phổ biến nhất — gọi API lần đầu)
useEffect(() => {
fetchUserData();
}, []); // Dependency array rỗng
// 3. Chạy khi dependency thay đổi
useEffect(() => {
fetchProducts(categoryId);
}, [categoryId]); // Chạy lại khi categoryId thay đổifunction UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// AbortController để hủy request nếu component unmount
const controller = new AbortController();
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
const res = await fetch(`/api/users/${userId}`, {
signal: controller.signal
});
if (!res.ok) throw new Error('Lỗi tải dữ liệu');
const data = await res.json();
setUser(data);
} catch (err) {
if (err.name !== 'AbortError') { // Bỏ qua lỗi do cleanup
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchUser();
// Cleanup: hủy request khi userId thay đổi hoặc unmount
return () => controller.abort();
}, [userId]);
if (loading) return <p>Đang tải...</p>;
if (error) return <p>Lỗi: {error}</p>;
return <div>{user?.name}</div>;
}// ❌ Infinite loop — object/array mới được tạo mỗi lần render
const [filters, setFilters] = useState({ page: 1 });
useEffect(() => {
fetchData(filters);
}, [filters]); // filters là object mới mỗi render → chạy mãi!
// ✅ Giải pháp: dùng giá trị primitive làm dependency
const [page, setPage] = useState(1);
useEffect(() => {
fetchData({ page });
}, [page]); // primitive → so sánh đượcReact sẽ re-render component khi:
- State thay đổi (
useStatesetter được gọi) - Props thay đổi (component cha truyền props mới)
- Component cha re-render (dù props con không đổi — đây là điểm nhiều người bỏ qua!)
- Context thay đổi (
useContext)
ParentComponent re-renders
↓
ChildComponent cũng re-renders (dù props không đổi!)
↓
GrandChildComponent cũng re-renders
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Tăng ({count})</button>
<Child /> {/* ← Re-render mỗi khi Parent re-render, dù không nhận props gì! */}
</div>
);
}
function Child() {
console.log('Child render'); // Sẽ log mỗi lần Parent re-render
return <p>Tôi là child</p>;
}// React.memo: chỉ re-render khi props thực sự thay đổi
const Child = React.memo(function Child({ name }) {
console.log('Child render');
return <p>Xin chào {name}</p>;
});
// ⚠️ Lưu ý: nếu props là function/object, cần kết hợp useCallback/useMemo// React 18+: tự động batch cả trong async
const handleClick = async () => {
// React sẽ BATCH 3 setState này → chỉ re-render 1 lần
setLoading(true);
setData(await fetchData());
setLoading(false);
};MOUNT UPDATE UNMOUNT
│ │ │
▼ ▼ ▼
render() render() cleanup()
│ │ (return của
▼ ▼ useEffect)
useEffect() useEffect()
([] - một lần) (khi dep thay đổi)
function LifecycleDemo({ id }) {
const [data, setData] = useState(null);
// ===== MOUNT (componentDidMount) =====
useEffect(() => {
console.log('✅ Component mounted');
// Khởi tạo: gọi API, đăng ký event, set timer...
const timer = setInterval(() => console.log('tick'), 1000);
// ===== UNMOUNT (componentWillUnmount) =====
return () => {
console.log('🔴 Component unmounted');
clearInterval(timer); // Dọn dẹp!
};
}, []);
// ===== UPDATE (componentDidUpdate) =====
useEffect(() => {
console.log('🔄 id thay đổi:', id);
fetchData(id).then(setData);
}, [id]); // Chạy lại khi id thay đổi
// ===== BEFORE RENDER (render phase) =====
// Không có side effects ở đây!
return <div>{data?.name}</div>;
}| Class Component | Functional Component (Hooks) |
|---|---|
componentDidMount |
useEffect(() => {}, []) |
componentDidUpdate |
useEffect(() => {}, [dep]) |
componentWillUnmount |
return () => {} trong useEffect |
shouldComponentUpdate |
React.memo |
getDerivedStateFromProps |
Tính toán trực tiếp trong render |
Virtual DOM là bản copy nhẹ (JavaScript object) của Real DOM. Thay vì thao tác trực tiếp vào DOM (chậm), React thao tác trên Virtual DOM (nhanh) rồi tính toán sự khác biệt.
State thay đổi
│
▼
React tạo Virtual DOM MỚI
│
▼
So sánh (Diffing/Reconciliation) với Virtual DOM CŨ
│
▼
Tính ra các thay đổi tối thiểu (Patches)
│
▼
Chỉ cập nhật những phần thay đổi vào Real DOM
// ❌ Không dùng React — cập nhật cả list dù chỉ thêm 1 item
document.getElementById('list').innerHTML = items.map(i => `<li>${i}</li>`).join('');
// ✅ React — chỉ thêm 1 <li> mới vào DOM thực
setItems(prev => [...prev, newItem]);// ❌ Dùng index làm key — gây bug khi sort/delete
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}
// ✅ Dùng ID duy nhất làm key
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}Giải thích: Khi bạn xóa item giữa list, nếu dùng
indexlàm key, React sẽ nghĩ item đầu tiên không thay đổi (vẫn key=0) và update sai. Dùngidduy nhất giúp React track đúng từng item.
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null); // Lưu interval ID, không gây re-render
const start = () => {
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
// useRef cũng dùng để truy cập DOM element trực tiếp
const inputRef = useRef(null);
const focusInput = () => inputRef.current.focus();
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>Focus Input</button>
<p>{seconds}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}// Dùng khi truyền function xuống component con được wrap bởi React.memo
const handleDelete = useCallback((id) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []); // Chỉ tạo lại function khi dependency thay đổi// Chỉ tính lại khi items hoặc filter thay đổi
const filteredItems = useMemo(() => {
return items.filter(item => item.category === filter)
.sort((a, b) => b.price - a.price);
}, [items, filter]);todo-app/
├── src/
│ ├── components/
│ │ ├── TodoInput.jsx ← Nhận input, gọi callback prop
│ │ ├── TodoItem.jsx ← Hiển thị 1 todo, toggle, delete
│ │ └── TodoList.jsx ← Render list TodoItem
│ ├── App.jsx ← Quản lý state todos[]
│ └── main.jsx
Yêu cầu:
- Thêm todo mới
- Đánh dấu todo hoàn thành / chưa hoàn thành
- Xóa todo
- Filter: All / Active / Completed
- Đếm số todo còn lại
Mở rộng: Lưu todos vào localStorage bằng useEffect
github-search/
├── src/
│ ├── components/
│ │ ├── SearchBar.jsx ← Input tìm kiếm với debounce
│ │ ├── UserCard.jsx ← Hiển thị thông tin user
│ │ └── RepoList.jsx ← Danh sách repo của user
│ ├── hooks/
│ │ └── useDebounce.js ← Custom hook debounce
│ ├── App.jsx
│ └── main.jsx
Yêu cầu:
- Tìm kiếm user GitHub qua API:
https://api.github.com/users/{username} - Debounce input 500ms (tránh gọi API liên tục)
- Hiển thị: avatar, tên, bio, số followers, số repos
- Hiển thị danh sách repos (gọi API thứ 2)
- Xử lý loading state và error state
- Cleanup AbortController khi username thay đổi
shopping-cart/
├── src/
│ ├── components/
│ │ ├── ProductGrid.jsx ← Danh sách sản phẩm
│ │ ├── ProductCard.jsx ← 1 sản phẩm (React.memo)
│ │ ├── Cart.jsx ← Giỏ hàng
│ │ └── CartItem.jsx ← 1 item trong giỏ
│ ├── App.jsx
│ └── main.jsx
Yêu cầu:
- Fetch danh sách products từ
https://fakestoreapi.com/products - Thêm/xóa sản phẩm vào giỏ
- Thay đổi số lượng từng item
- Tính tổng tiền (dùng useMemo)
- Filter theo category
- Giỏ hàng lưu vào localStorage
- Tối ưu re-render với React.memo + useCallback
Hoàn thành các project xong, hãy đảm bảo bạn trả lời được:
- Sự khác biệt giữa
propsvàstatelà gì? - Khi nào dùng
useEffectvới[]và khi nào có dependency? - Tại sao KHÔNG được mutate state trực tiếp?
- Tại sao phải có
keykhi render list? -
useRefkhácuseStateở điểm gì? - Khi nào nên dùng
React.memo,useCallback,useMemo? - Virtual DOM giúp React tối ưu performance như thế nào?
| Tài nguyên | Link | Ghi chú |
|---|---|---|
| React Docs (Official) | https://react.dev | Tài liệu chính thức, rất tốt |
| Beta Docs Tutorial | https://react.dev/learn | Học theo project |
| React DevTools | Chrome Extension | Debug re-render trực quan |
| Fake API | https://jsonplaceholder.typicode.com | API giả cho practice |
| Fake Store API | https://fakestoreapi.com | API sản phẩm cho project 3 |