function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount((c) => c + 1);
console.log(count);
}
return <button onClick={handleClick}>{count}</button>;
}
React
draftedRendering & reconciliation, hooks pitfalls, effects, memoization vs React Compiler, concurrent features, React 19 Actions (`use`, `useActionState`, `useOptimistic`), error boundaries, testing — theo React 19.2.
0/65 rated · mastery 0%
Bài học
12 bài · ~153 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~14 phút· 5 câuverify
Render, commit và state snapshot
Cái gì trigger render, render khác commit ra sao, state là snapshot, batching, updater function, immutability và useState vs useReducer.
- 02~13 phút· 4 câu
Reconciliation, key và vòng đời state
React giữ hay huỷ state theo vị trí + type + key, key trong list, reset state bằng key, component khai báo lồng, portal và event bubbling.
- 03~13 phút· 6 câuverify
Hooks: Rules of Hooks, custom hook, useRef và ref trong React 19
Vì sao hook phụ thuộc thứ tự gọi, custom hook chia sẻ logic không chia sẻ state, useRef, ref là prop, ref cleanup, map lifecycle class sang hook.
- 04~13 phút· 6 câuverify
Effects: đồng bộ với hệ thống bên ngoài
Vòng đời setup/cleanup, StrictMode chạy effect hai lần, useEffect vs useLayoutEffect, khi nào không cần effect, stale closure, useEffectEvent, vòng lặp render vô hạn.
- 05~13 phút· 7 câuverify
Memoization và React Compiler
memo, useMemo, useCallback làm gì và khi nào vô dụng, identity của object/function, composition thay memo, React Compiler và cách rollout.
- 06~13 phút· 4 câuverify
Concurrent rendering: lanes, transition, deferred value, Activity
Update priority (lanes), render ngắt được còn commit thì không, useTransition vs useDeferredValue, tearing và useSyncExternalStore, Activity.
- 07~12 phút· 6 câuverify
Suspense, lazy và error boundaries
Suspense boundary, lazy/code splitting, transition giữ UI cũ, ChunkLoadError sau deploy, error boundary bắt gì và không bắt gì, báo lỗi ở React 19.
- 08~14 phút· 6 câuverify
Data fetching, SSR, hydration và Server Components
Race condition khi fetch trong effect, vì sao fetch trong effect là anti-pattern, use(promise), CSR vs SSR vs RSC, hydration mismatch, cân nhắc migrate sang Next.js.
- 09~12 phút· 5 câuverify
Forms và React 19 Actions
Controlled vs uncontrolled, form action, useActionState, useFormStatus, useOptimistic, cursor nhảy trong input controlled, thiết kế hệ thống form lớn.
- 10~12 phút· 6 câuverify
Kiến trúc state, Context và component API
Đặt state ở đâu, vấn đề hiệu năng của Context, component library và design system, multi-tenant theming, ranh giới feature trong app lớn.
- 11~13 phút· 6 câu
Chẩn đoán hiệu năng và UI realtime
Đo trước khi tối ưu, list 5.000 dòng, virtualization, feed 50 update/giây, performance budget, subscription Socket.IO không leak, load test frontend.
- 12~12 phút· 4 câuverify
Testing React: Testing Library, act, MSW
Query theo role, userEvent, findBy, cảnh báo act(), mock fetch vs module vs MSW, characterization test khi refactor legacy.
Tóm tắt & cheat sheet của track
TL;DR
- React = UI là function của state. Mỗi render là một snapshot (props + state + closure); React so sánh cây mới với cây cũ (reconciliation) rồi commit thay đổi tối thiểu vào DOM.
- Hầu hết bug React thật đến từ 4 chỗ: effect dùng sai (sync state bằng effect, thiếu cleanup, race condition), stale closure, identity (object/function mới mỗi render phá memo & deps), key sai (state dính nhầm item).
- React 18 đưa concurrent rendering (transition, deferred value, Suspense streaming SSR, automatic batching). React 19 đưa Actions (
useActionState,useOptimistic,useFormStatus,<form action>),use(),reflà prop,<Context>làm provider. React 19.2 thêm<Activity>vàuseEffectEvent. - React Compiler (1.0, 2025) tự memoize →
useMemo/useCallback/memothủ công ít cần hơn, nhưng code phải tuân Rules of React (render thuần, không mutate props/state). - Senior bar: biết đo (React Profiler, Performance panel) trước khi tối ưu; biết đặt state ở đâu (local, lifted, URL, server cache, global store).
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Render vs commit | Render = gọi component tính JSX (thuần, có thể bị gọi lại/bỏ); commit = áp vào DOM + chạy effect | console.log trong body chạy 2 lần ở StrictMode dev |
| Reconciliation | So cây cũ/mới theo type + vị trí + key | Đổi <Input> thành <textarea> cùng vị trí → unmount state |
| Key | Danh tính của phần tử trong list, quyết định state đi theo ai | key={item.id}, không phải index khi list đổi thứ tự |
| Snapshot / closure | Handler thấy state của render đã tạo ra nó | setCount(count + 1) 3 lần → +1 |
| Effect | Đồng bộ component với hệ thống bên ngoài React | subscribe WebSocket, đổi document.title |
| Memoization | Giữ lại value/function/render giữa các render nếu deps không đổi | memo(Row) + useCallback(onSelect) |
| Transition | Đánh dấu update không khẩn cấp, có thể bị ngắt | startTransition(() => setFilter(q)) |
| Suspense | Boundary hiển thị fallback khi con đang "chờ" (lazy, use(promise), data framework) | <Suspense fallback={<Skeleton/>}> |
| Action | Hàm (có thể async) chạy trong transition, gắn với form/pending/optimistic | <form action={saveAction}> |
| Error boundary | Class component bắt lỗi khi render của cây con | static getDerivedStateFromError |
Cơ chế hoạt động
Vòng đời một update
Reconciliation quyết định giữ hay huỷ state
React 19 Action với form
Trade-offs
| Quyết định | Lựa chọn A | Lựa chọn B | Khi nào chọn |
|---|---|---|---|
| Memo thủ công vs React Compiler | memo/useMemo/useCallback có chủ đích | Compiler tự memoize | Compiler khi codebase tuân Rules of React; thủ công cho hot path đã đo |
| Controlled vs uncontrolled input | State React là nguồn thật (validate khi gõ) | DOM giữ giá trị, đọc lúc submit (FormData) | Form lớn/đơn giản → uncontrolled + Actions; cần format/validate realtime → controlled |
| Context vs store (Redux/Zustand) | Context: dependency injection, ít đổi | Store có selector, tránh re-render toàn cây | Theme/auth/locale → context; state đổi thường xuyên nhiều consumer → store |
| Fetch trong effect vs data library/RSC | Tự viết: race, cache, retry đều tự lo | React Query/SWR/RSC | Hầu như luôn dùng library/framework |
| Class vs function component | Class: lifecycle, error boundary | Hooks: tái sử dụng logic, compiler hỗ trợ | Code mới dùng function; class chỉ cho error boundary/legacy |
useTransition vs useDeferredValue | Bọc setter mình kiểm soát | Trì hoãn value nhận từ ngoài | Có quyền với setState → transition; chỉ có prop → deferred |
Pitfalls
- Dùng
useEffectđể tính derived state (setFullName(first + last)) → render thừa, lệch một nhịp. Tính trực tiếp trong render. - Fetch trong effect không cleanup/ignore flag → race condition (response cũ đè response mới).
- Object/array/function literal truyền xuống
memochild hoặc deps → memo vô dụng, effect chạy mãi. key={index}hoặckey={Math.random()}→ state dính sai dòng / remount mỗi render.- Mutate state (
arr.push) rồisetArr(arr)→ cùng reference, React bỏ qua render. - Nghĩ error boundary bắt lỗi trong event handler/
setTimeout/promise — không. - Đặt Context provider value là object mới mỗi render → mọi consumer re-render.
- Test implementation details (state nội bộ, class name) thay vì hành vi người dùng.
Cheat sheet
- Rules of Hooks: chỉ gọi ở top level, chỉ trong component/custom hook.
use()là ngoại lệ: gọi được trongif/loop. - Updater function khi state mới phụ thuộc state cũ:
setN(n => n + 1). Updater phải thuần. - Effect: setup + cleanup đối xứng; StrictMode dev chạy setup → cleanup → setup để lộ thiếu cleanup.
useLayoutEffect: đo DOM trước paint (tooltip position). Mặc định dùnguseEffect.useRef: giá trị mutable không gây render; không đọc/ghiref.currenttrong render.- React 19:
reflà prop thường cho function component;<Ctx value={v}>thay<Ctx.Provider>; ref callback có thể trả cleanup. - React 19 Actions:
useActionState(fn, init)→[state, formAction, isPending];useOptimistic(state, reducer);useFormStatus()(react-dom) đọc form cha. - React 19.2:
<Activity mode="hidden">giữ state nhưng unmount effect;useEffectEventtách logic không reactive khỏi deps. - Error boundary: vẫn phải là class (hoặc thư viện
react-error-boundary); không bắt lỗi event handler/async. - Test:
getByRole>getByText>getByTestId;findBy*cho async;userEventthayfireEvent; mock network bằng MSW.
References
- react.dev — Render and Commit ↗
- react.dev — Preserving and Resetting State ↗
- react.dev — You Might Not Need an Effect ↗
- react.dev — Synchronizing with Effects ↗
- react.dev — React 19 release post ↗
- react.dev — React 19.2 release post ↗
- react.dev — React Compiler ↗
- react.dev — Rules of React ↗
- react.dev — useActionState ↗
- react.dev — useOptimistic ↗
- react.dev — use ↗
- react.dev — useTransition ↗
- react.dev — Component (class) and error boundaries ↗
- Testing Library — Guiding principles & queries ↗
Interview Questions
Showing 65 / 65
function Search({ query }: { query: string }) {
const [results, setResults] = useState<Item[]>([]);
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then((r) => r.json())
.then(setResults);
}, [query]);
return <List items={results} />;
}
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = setInterval(() => setSeconds(seconds + 1), 1000);
return () => clearInterval(id);
}, []);
return <p>{seconds}</p>;
}
type State = { error?: string; ok?: boolean };
async function saveEmail(_prev: State, formData: FormData): Promise<State> {
const email = String(formData.get('email'));
const res = await fetch('/api/profile', { method: 'POST', body: JSON.stringify({ email }) });
if (res.status === 422) return { error: 'Email is invalid' };
return { ok: true };
}
function EmailForm() {
const [state, formAction, isPending] = useActionState(saveEmail, {});
return (
<form action={formAction}>
<input name="email" defaultValue="" />
<button disabled={isPending}>{isPending ? 'Saving…' : 'Save'}</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
);
}
function Comments({ comments, send }: { comments: Comment[]; send: (t: string) => Promise<void> }) {
const [optimistic, addOptimistic] = useOptimistic(
comments,
(list, text: string) => [...list, { id: 'temp', text, pending: true }],
);
async function action(formData: FormData) {
const text = String(formData.get('text'));
addOptimistic(text);
await send(text); // throws → optimistic comment disappears
}
return (
<form action={action}>
{optimistic.map((c) => <p key={c.id} style={{ opacity: c.pending ? 0.5 : 1 }}>{c.text}</p>)}
<input name="text" />
</form>
);
}
function ColorPicker({ children }: { children: React.ReactNode }) {
const [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
{children /* not re-rendered when color changes */}
</div>
);
}
// <ColorPicker><ExpensiveTree /></ColorPicker>
function useOnline() {
return useSyncExternalStore(
(cb) => {
window.addEventListener('online', cb);
window.addEventListener('offline', cb);
return () => { window.removeEventListener('online', cb); window.removeEventListener('offline', cb); };
},
() => navigator.onLine,
() => true, // server snapshot
);
}
render(<LoginForm />);
await userEvent.type(screen.getByLabelText(/email/i), 'a@b.co');
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
expect(await screen.findByRole('alert')).toHaveTextContent(/invalid password/i);
function Todos() {
const [todos, setTodos] = useState<Todo[]>([]);
function add(title: string) {
todos.push({ id: crypto.randomUUID(), title });
setTodos(todos);
console.log(todos.length); // increases
}
return <TodoList items={todos} onAdd={add} />;
}
function Report({ filters }: { filters: Filters }) {
const [data, setData] = useState<Row[]>([]);
const query = { ...filters, page: 1 };
useEffect(() => {
fetchReport(query).then(setData);
}, [query]);
const [total, setTotal] = useState(0);
setTotal(data.length);
return <Table rows={data} />;
}
function ChatRoom({ roomId, muted }: { roomId: string; muted: boolean }) {
const onMessage = useEffectEvent((msg: Message) => {
if (!muted) playSound();
addMessage(msg);
});
useEffect(() => {
const conn = connect(roomId);
conn.on('message', onMessage);
return () => conn.close();
}, [roomId]); // muted changes do NOT reconnect
}
const Row = memo(function Row({ item, onSelect, style }: RowProps) {
return <li style={style} onClick={() => onSelect(item.id)}>{item.name}</li>;
});
function List({ items }: { items: Item[] }) {
const [selected, setSelected] = useState<string | null>(null);
const handleSelect = useCallback((id: string) => {
setSelected(id);
track('select', { id, previous: selected });
}, [selected]);
return (
<ul>
{items.map((item) => (
<Row key={item.id} item={item} onSelect={handleSelect} style={{ padding: 8 }} />
))}
</ul>
);
}
server.use(
http.get('/api/orders', () => HttpResponse.json({ message: 'boom' }, { status: 500 })),
);
render(<Orders />, { wrapper: Providers });
expect(await screen.findByRole('alert')).toHaveTextContent(/could not load orders/i);