Skip to content
DevRecall
← Roadmap
09

React

drafted

Rendering & 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.
  1. 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.

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

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

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

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

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

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

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

  9. 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. 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. 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~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(), ref là prop, <Context> làm provider. React 19.2 thêm <Activity> và useEffectEvent.
  • React Compiler (1.0, 2025) tự memoize → useMemo/useCallback/memo thủ 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ệmMột câuVí dụ ngắn
Render vs commitRender = gọi component tính JSX (thuần, có thể bị gọi lại/bỏ); commit = áp vào DOM + chạy effectconsole.log trong body chạy 2 lần ở StrictMode dev
ReconciliationSo cây cũ/mới theo type + vị trí + keyĐổi <Input> thành <textarea> cùng vị trí → unmount state
KeyDanh tính của phần tử trong list, quyết định state đi theo aikey={item.id}, không phải index khi list đổi thứ tự
Snapshot / closureHandler 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 Reactsubscribe WebSocket, đổi document.title
MemoizationGiữ lại value/function/render giữa các render nếu deps không đổimemo(Row) + useCallback(onSelect)
TransitionĐánh dấu update không khẩn cấp, có thể bị ngắtstartTransition(() => setFilter(q))
SuspenseBoundary hiển thị fallback khi con đang "chờ" (lazy, use(promise), data framework)<Suspense fallback={<Skeleton/>}>
ActionHàm (có thể async) chạy trong transition, gắn với form/pending/optimistic<form action={saveAction}>
Error boundaryClass component bắt lỗi khi render của cây constatic 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 địnhLựa chọn ALựa chọn BKhi nào chọn
Memo thủ công vs React Compilermemo/useMemo/useCallback có chủ đíchCompiler tự memoizeCompiler khi codebase tuân Rules of React; thủ công cho hot path đã đo
Controlled vs uncontrolled inputState 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 đổiStore có selector, tránh re-render toàn câyTheme/auth/locale → context; state đổi thường xuyên nhiều consumer → store
Fetch trong effect vs data library/RSCTự viết: race, cache, retry đều tự loReact Query/SWR/RSCHầu như luôn dùng library/framework
Class vs function componentClass: lifecycle, error boundaryHooks: tái sử dụng logic, compiler hỗ trợCode mới dùng function; class chỉ cho error boundary/legacy
useTransition vs useDeferredValueBọc setter mình kiểm soátTrì hoãn value nhận từ ngoàiCó 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 memo child hoặc deps → memo vô dụng, effect chạy mãi.
  • key={index} hoặc key={Math.random()} → state dính sai dòng / remount mỗi render.
  • Mutate state (arr.push) rồi setArr(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 trong if/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ùng useEffect.
  • useRef: giá trị mutable không gây render; không đọc/ghi ref.current trong render.
  • React 19: ref là 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; useEffectEvent tá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; userEvent thay fireEvent; mock network bằng MSW.

Personal notes (Notion)

Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.

Interview Questions

Showing 65 / 65

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>;
}
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);