Menu

Câu hỏi phỏng vấn React kèm câu trả lời (React 19)

Khoảng 30 câu hỏi phỏng vấn React từ cơ bản đến nâng cao, mỗi câu có một câu trả lời ngắn mà nhà tuyển dụng chấp nhận, kèm code chạy trực tiếp cho những câu dễ hiểu khi nhìn hơn là khi đọc.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Đây là những câu hỏi phỏng vấn React xuất hiện thường xuyên nhất, được chia theo cấp độ và trả lời theo cách nhà tuyển dụng mong đợi: ngắn gọn, đúng với React 19, và có lý do đằng sau mỗi quy tắc. Ở những chỗ nhìn hành vi dễ hiểu hơn đọc mô tả, câu trả lời có một trình soạn thảo chạy trực tiếp, và vài câu hỏi yêu cầu bạn đoán đầu ra của console trước khi chạy.

Trước khi nhấn: console bên dưới bản xem trước in ra gì, và nút hiển thị gì? Câu trả lời nằm trong phần câu hỏi cơ bản bên dưới.

Câu hỏi cơ bản

React là gì?

React là một thư viện JavaScript để xây dựng giao diện người dùng từ các component. Bạn viết các hàm trả về bản mô tả giao diện cho dữ liệu hiện tại, và khi dữ liệu đó thay đổi, React tự tìm ra phần nào thay đổi và cập nhật DOM cho bạn. React chỉ lo phần hiển thị (view): định tuyến, lấy dữ liệu và form đến từ các thư viện khác hoặc từ một framework như Next.js.

JSX là gì?

JSX là một phần mở rộng cú pháp cho phép bạn viết markup bên trong JavaScript. Trình biên dịch (Babel, esbuild, SWC) biến mỗi thẻ thành một lời gọi tạo React element, nên <p className="note">{text}</p> trở thành một object có type: 'p' cùng các props. Vì JSX là JavaScript, bạn dùng className thay vì class, đặt biểu thức trong {}, và mọi thẻ đều phải được đóng. Trang JSX trình bày chi tiết các quy tắc.

Component và element khác nhau thế nào?

Component là một hàm (hoặc class) nhận props và trả về giao diện. Element là object thuần mà JSX tạo ra, mô tả thứ cần render: <Avatar size={40} /> là một element có type là hàm Avatar. React gọi component của bạn để biến element thành các element khác, xuống tận các thẻ DOM. Element rẻ và bất biến; bạn không bao giờ tự gọi một component.

Props và state khác nhau thế nào?

Props do component cha truyền vào và chỉ được đọc bên trong component. State do component sở hữu, thay đổi theo thời gian qua một setter, và mỗi lần thay đổi sẽ lên lịch một lần render mới. Một cách nói phổ biến: props cấu hình component, còn state là bộ nhớ của nó. Cùng một giá trị có thể là state ở component cha và là prop ở component con.

Ví dụ đầu tiên trên trang này in ra gì?

Nó in ra logged: 0, và nút hiển thị Count: 1, không phải 2. State là một bản chụp: count là hằng số trong suốt lần render, nên cả hai lời gọi setCount(count + 1) đều tính 0 + 1, và log đọc giá trị cũ vì giá trị mới chỉ tồn tại ở lần render tiếp theo. Để cộng thêm hai, hãy dùng dạng hàm cập nhật: gọi setCount((c) => c + 1) hai lần.

Vì sao danh sách cần key, và vì sao chỉ số là key tồi?

React dùng key để khớp từng mục trong danh sách với mục tương ứng ở lần render trước. Nếu bạn dùng chỉ số mảng và chèn một mục lên đầu, mọi chỉ số đều dịch đi, nên React giữ state của từng hàng ở vị trí cũ và gắn nó vào sai dữ liệu.

Đánh dấu Ada, rồi nhấn "Add to top": dấu tích nhảy sang hàng đầu tiên mới. Đổi key={index} thành key={person.id} và thử lại, dấu tích sẽ ở lại với Ada. Trong môi trường development, React còn cảnh báo khi thiếu key; bản xem trước chạy như bản build production, nên bạn sẽ không thấy cảnh báo đó ở đây.

Ô nhập có kiểm soát (controlled input) là gì?

Là một ô nhập có giá trị lấy từ state của React: bạn truyền value và cập nhật state trong onChange, nên React là nguồn dữ liệu duy nhất và bạn có thể kiểm tra hoặc biến đổi từng lần gõ phím. Ô nhập không kiểm soát giữ giá trị riêng của nó trong DOM và bạn đọc giá trị khi cần, qua một ref hoặc FormData của form. Ô nhập có kiểm soát là lựa chọn mặc định khi giao diện phản ứng theo những gì người dùng gõ.

Lifting state up nghĩa là gì?

Khi hai component cần cùng một dữ liệu, bạn chuyển state lên component cha chung gần nhất và truyền nó xuống dưới dạng props, kèm một hàm để thay đổi nó. Các component con không còn giữ bản sao riêng, nên chúng không bao giờ lệch nhau. Đây là câu trả lời tiêu chuẩn cho câu hỏi "các component anh em chia sẻ state thế nào" trước khi nghĩ đến context hay một store.

Rules of Hooks là gì?

Chỉ gọi hook ở cấp cao nhất của component hoặc custom hook, không bao giờ trong điều kiện, vòng lặp hay hàm lồng nhau, và chỉ gọi từ các hàm React, không gọi từ hàm JavaScript thông thường. React nhận diện mỗi hook theo thứ tự các lời gọi, nên thứ tự đó phải giống hệt nhau ở mọi lần render. Package eslint-plugin-react-hooks kiểm tra cả hai quy tắc này.

Fragment là gì?

Fragment nhóm nhiều phần tử lại mà không thêm node DOM nào: <>...</>, hoặc <Fragment key={id}> khi bạn cần key trong một danh sách. Một component phải trả về một gốc duy nhất, và fragment cho phép gốc đó là nhiều phần tử anh em, điều này quan trọng trong bảng và bố cục flex, nơi một div thừa sẽ làm hỏng markup.

Câu hỏi trung cấp

Mảng dependency của useEffect hoạt động thế nào?

React chạy effect sau khi lần render được commit (thường là sau khi trình duyệt vẽ), rồi chạy lại sau mỗi lần render tiếp theo mà một giá trị trong mảng thay đổi (so sánh bằng Object.is). Không có mảng thì effect chạy sau mọi lần render; với [] nó chạy một lần sau lần render đầu tiên. Mọi giá trị của component mà effect đọc đều phải nằm trong mảng, và linter sẽ báo khi thiếu.

Khi nào hàm cleanup của effect chạy?

Hàm bạn trả về từ effect chạy trước khi effect chạy lại với dependency mới, và một lần khi component unmount. Đó là nơi bạn hoàn tác những gì effect đã thiết lập: xóa timer, gỡ listener, đóng kết nối.

Nhấn "Switch room" và theo dõi console: cleanup cho phòng cũ chạy trước, sau đó effect kết nối tới phòng mới. "Hide chat" chỉ chạy cleanup. Trong môi trường development, StrictMode thêm một chu kỳ kết nối, ngắt kết nối, kết nối khi mount để kiểm tra cleanup của bạn có hoạt động không; bản xem trước chạy như bản build production, nên bạn chỉ thấy trình tự thật.

Vì sao state trông như bị cũ bên trong setInterval?

Callback của interval là một closure giữ count của lần render đã tạo ra nó. Với mảng dependency rỗng, effect không bao giờ chạy lại, nên callback cứ đọc mãi giá trị count đầu tiên đó.

broken lên tới 1 rồi dừng, vì mỗi nhịp đều tính 0 + 1. fixed tiếp tục đếm, vì hàm cập nhật nhận giá trị mới nhất. Thêm broken vào mảng dependency cũng được, nhưng phải trả giá bằng việc xóa và tạo lại interval mỗi giây.

Batching là gì?

React gom nhiều lần cập nhật state vào một lần render duy nhất. Từ React 18, điều này áp dụng ở mọi nơi: trình xử lý sự kiện, timeout, promise và listener sự kiện gốc. Nếu bạn cần DOM được cập nhật ngay sau một lần cập nhật, flushSync từ react-dom cho phép bỏ qua batching, nhưng bạn hiếm khi cần đến nó.

Lần nhấn chỉ ghi một dòng render 1 1 1, không phải ba lần render. Trước React 18, ba lần cập nhật state bên trong một timeout từng đồng nghĩa với ba lần render.

Đoạn này in ra gì? (thứ tự effect)

Đọc code và viết ra bốn dòng mà console sẽ in, theo thứ tự, trước khi bạn xem.

Render đi từ trên xuống: render App, rồi render Child. Effect chạy từ dưới lên, sau khi cả cây đã được commit: effect Child, rồi effect App. Effect của component cha có thể dựa vào việc effect của các component con đã chạy xong.

useMemo và useCallback khác nhau thế nào?

useMemo(() => compute(a, b), [a, b]) lưu cache kết quả của một phép tính. useCallback(fn, [deps]) lưu cache chính hàm đó, nên nó tương đương useMemo(() => fn, [deps]). Cả hai chỉ có ích khi một thứ phía sau so sánh theo tham chiếu: một component con dùng memo, hoặc mảng dependency của một hook khác. Từ React Compiler 1.0 (tháng 10 năm 2025), một bước build có thể tự thêm phần memo hóa này, nhưng memo hóa thủ công vẫn hoạt động và bạn vẫn sẽ gặp nó trong phần lớn các codebase.

React.memo làm gì, và vì sao nó thường cần useCallback?

memo(Component) bỏ qua việc render lại component khi props giống hệt lần trước, so sánh từng cái bằng Object.is. Một hàm viết inline trong component cha là một hàm mới ở mỗi lần render, nên nó được tính là prop đã thay đổi và làm memo mất tác dụng. useCallback giữ nguyên cùng một hàm giữa các lần render.

Xóa console rồi nhấn "Re-render parent": chỉ render inline handler xuất hiện. Component con còn lại nhận cùng một hàm và được bỏ qua. Bỏ memo(...) quanh Button và cả hai đều ghi log ở mỗi lần nhấn, cho thấy chỉ riêng useCallback thì không làm được gì.

Context là gì, và nó tốn chi phí gì?

Context truyền một giá trị cho mọi component bên dưới một provider mà không cần truyền qua props: createContext, rồi <ThemeContext value={theme}> trong React 19 (hoặc <ThemeContext.Provider>), và useContext(ThemeContext) để đọc. Chi phí là mọi component đọc context đều render lại khi giá trị thay đổi, và memo không chặn được điều này. Hãy giữ chi phí thấp bằng cách tách các giá trị không liên quan vào các context riêng, bọc giá trị là object trong useMemo, và không đặt state thay đổi nhanh vào context toàn ứng dụng. Xem useContext.

Ref là gì, và khi nào dùng ref thay cho state?

useRef(initial) trả về một object có thuộc tính current được giữ nguyên qua các lần render, và thay đổi nó không kích hoạt render. Dùng ref cho những giá trị mà màn hình không phụ thuộc vào (id của timer, giá trị trước đó, một cờ) và để truy cập DOM (<input ref={inputRef}> rồi inputRef.current.focus()). Nếu giá trị được hiển thị trên màn hình, nó thuộc về state.

Virtual DOM là gì, và reconciliation là gì?

Ở mỗi lần render, React tạo một cây element mới và so sánh với cây trước; phép so sánh này là reconciliation. Nếu loại của một element thay đổi (một div thành section, hoặc ProfileA thành ProfileB), React hủy cây con đó cùng state của nó và dựng một cây mới. Nếu loại giữ nguyên, nó giữ node DOM và cập nhật các props đã thay đổi, và dùng key để khớp các phần tử con trong danh sách. "Virtual DOM" là tên gọi phổ biến của cây trong bộ nhớ đó; tốc độ đến từ việc chỉ động vào những gì thay đổi, không phải vì bản thân cây đó nhanh.

Custom hook là gì?

Là một hàm có tên bắt đầu bằng use và gọi các hook khác, để bạn tái sử dụng logic có state giữa các component: useOnlineStatus, useLocalStorage, useDebouncedValue. Mỗi component gọi nó có state độc lập riêng; custom hook chia sẻ logic, không chia sẻ dữ liệu. Xem custom hook.

StrictMode làm gì?

<StrictMode> bật thêm các kiểm tra chỉ trong môi trường development. Nó render component hai lần để lộ ra việc render không thuần, chạy effect theo chu kỳ setup, cleanup, setup một lần khi mount để lộ ra cleanup bị thiếu, làm tương tự với ref callback, và cảnh báo về các API đã deprecated. Không điều nào trong số này xảy ra ở bản build production, nên việc console log hai lần trong development là bình thường, không phải bug.

Câu hỏi nâng cao

Đoạn này in ra gì? (render, layout effect, effect)

Thêm một câu đoán nữa. Ba dòng log, ba thời điểm khác nhau trong một lần render.

Thứ tự là render, rồi useLayoutEffect, rồi useEffect, dù useEffect được viết trước. Layout effect chạy sau khi React cập nhật DOM nhưng trước khi trình duyệt vẽ, vì vậy nó phù hợp để đo kích thước một phần tử; effect thông thường chạy muộn hơn, thường là sau khi vẽ, và nên là lựa chọn mặc định.

Error boundary là gì?

Là một class component bắt các lỗi được ném ra trong lúc render các component con của nó và hiển thị giao diện thay thế (fallback) thay vì unmount cả ứng dụng. Nó cần static getDerivedStateFromError (để chuyển sang fallback) và thường có thêm componentDidCatch (để ghi log). Hiện vẫn chưa có hook tương đương, nên trong các codebase dùng function component, các đội tự viết một class hoặc dùng package react-error-boundary.

Nhấn "Break it": fallback chỉ thay thế các component con của boundary, nút bấm vẫn còn, và React vẫn báo lỗi đã bắt được trong console. Boundary không bắt lỗi trong trình xử lý sự kiện, trong code bất đồng bộ như setTimeout, hoặc trong chính boundary. Với những trường hợp đó, bạn dùng try/catch và cập nhật state.

Portal là gì?

createPortal(children, domNode) từ react-dom render các phần tử con vào một node DOM khác, thường là document.body, trong khi vẫn giữ chúng ở cùng vị trí trong cây React. Modal, tooltip và toast dùng nó để thoát khỏi overflow: hidden hoặc stacking context của component cha. Context vẫn hoạt động xuyên qua portal, và sự kiện nổi bọt theo cây React, nên một lần click bên trong portal vẫn tới được onClick của component cha dù các node DOM nằm ở chỗ khác. Xem portal.

Suspense và lazy là gì?

lazy(() => import('./Chart')) tải code của một component ở lần đầu nó render, giúp chia nhỏ bundle. <Suspense fallback={<Spinner />}> hiển thị fallback khi bất kỳ thứ gì bên trong vẫn đang tải: code lazy, hoặc dữ liệu được đọc bằng use(promise) hay qua một framework hỗ trợ Suspense. Vị trí đặt các boundary quyết định những phần nào của trang cùng chờ với nhau.

import { lazy, Suspense } from 'react';

const Chart = lazy(() => import('./Chart.jsx'));

export default function Dashboard() {
    return (
        <Suspense fallback={<p>Loading chart...</p>}>
            <Chart />
        </Suspense>
    );
}

useTransition làm gì?

Nó đánh dấu một lần cập nhật state là không khẩn cấp. const [isPending, startTransition] = useTransition() và startTransition(() => setTab('posts')) cho phép React giữ màn hình hiện tại phản hồi nhanh và ngắt lần render chậm nếu có một cập nhật khẩn cấp hơn xuất hiện, như một lần gõ phím. isPending báo cho bạn biết lúc cần hiển thị gợi ý đang tải. Trong React 19, hàm bạn truyền vào có thể là async, và React gọi đó là một action. Đừng dùng nó để cập nhật giá trị của chính một ô nhập văn bản; cập nhật đó phải khẩn cấp.

React Server Components là gì?

Là các component chỉ chạy trên máy chủ (hoặc lúc build) và gửi kết quả đã render tới trình duyệt, không bao giờ gửi code của chúng. Chúng có thể là async và đọc trực tiếp cơ sở dữ liệu hoặc hệ thống tệp, nhưng không dùng được state, effect hay API của trình duyệt. Một tệp được đánh dấu 'use client' bắt đầu một ranh giới phía client, nơi đặt các component tương tác, còn 'use server' đánh dấu các server function mà code phía client có thể gọi. Bạn dùng chúng thông qua một framework như Next.js với App Router; ứng dụng Vite thuần không có máy chủ để chạy chúng.

React 19 có gì thay đổi?

  • Action: hàm async trong transition, với trạng thái chờ được xử lý sẵn cho bạn. Một <form action={fn}> gọi fn với FormData của form.
  • Hook mới: useActionState cho kết quả và trạng thái chờ của form, useOptimistic cho giao diện lạc quan (optimistic UI), và useFormStatus trong react-dom.
  • use: đọc một promise (tạm dừng cho đến khi nó resolve) hoặc một context, và khác với các hook khác, nó có thể được gọi bên trong điều kiện.
  • ref là một prop: function component nhận ref như mọi prop khác, nên code mới không cần forwardRef nữa. Ref callback có thể trả về một hàm cleanup.
  • <Context> dùng làm provider, cùng các thẻ <title> và <meta> được tự động đưa lên phần head của tài liệu.
  • Những thứ bị loại bỏ: ReactDOM.render, string ref, legacy context, và defaultProps trên function component (hãy dùng tham số mặc định).
// React 19: ref is a normal prop
function TextInput({ ref, ...props }) {
    return <input ref={ref} {...props} />;
}

Trang React 19 có ví dụ chạy được cho từng mục.

Kiểm thử component React thế nào?

Dùng React Testing Library cùng một trình chạy test như Vitest hoặc Jest. Bạn render component, tìm phần tử theo cách người dùng tìm (theo role, nhãn hoặc văn bản), tương tác bằng user-event, và kiểm tra những gì hiển thị, không kiểm tra state nội bộ. Cách này giúp test vẫn đúng khi bạn refactor.

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';

test('counts clicks', async () => {
    const user = userEvent.setup();
    render(<Counter />);
    await user.click(screen.getByRole('button', { name: /clicked 0 times/i }));
    expect(screen.getByRole('button')).toHaveTextContent('Clicked 1 times');
});

toHaveTextContent đến từ @testing-library/jest-dom. Để kiểm thử cả luồng qua nhiều trang, các đội bổ sung thêm Playwright hoặc Cypress.

Tìm và sửa vấn đề hiệu năng thế nào?

Đo trước. Mở tab Profiler trong React DevTools, ghi lại thao tác chậm và đọc biểu đồ flame: nó cho thấy component nào đã render, mỗi component mất bao lâu, và (với tùy chọn "record why each component rendered") vì sao. Các cách sửa thường gặp, theo thứ tự nên thử: đưa state xuống thấp để ít component phụ thuộc vào nó hơn, truyền các cây con chậm qua children, memo hóa phần tốn kém bằng memo, useMemo và useCallback, ảo hóa danh sách dài, và hoãn các cập nhật nặng bằng useTransition hoặc useDeferredValue. Hãy profile trên bản build production hoặc bản build profiling; bản build development chậm hơn và phóng đại thời gian render.

Vì sao component nên thuần (pure)?

Với cùng props, state và context, một component nên trả về cùng JSX và không thay đổi gì bên ngoài nó trong lúc render. React có thể render một component nhiều lần, không theo thứ tự, hoặc bỏ đi một lần render (transition, Suspense, StrictMode), nên side effect trong thân component như sửa một biến hay gửi một request sẽ chạy một số lần không đoán trước được. Side effect trước hết thuộc về trình xử lý sự kiện, và thuộc về effect khi không có sự kiện nào gây ra chúng.

Khi nào không nên dùng effect?

Khi giá trị có thể tính được trong lúc render (danh sách đã lọc, họ tên đầy đủ, tổng cộng), khi công việc do hành động của người dùng gây ra (đặt nó trong trình xử lý sự kiện), và khi bạn muốn đặt lại state lúc một prop thay đổi (dùng key). Effect dùng để đồng bộ với thứ gì đó bên ngoài React: một kết nối mạng, một API của trình duyệt, một widget bên thứ ba. Chuỗi effect liên tiếp cập nhật state là nguồn gây ra render thừa và bug thường gặp.

Câu hỏi thường gặp

React là gì?

React là một thư viện JavaScript để xây dựng giao diện người dùng từ các component. Bạn mô tả màn hình cần trông ra sao với dữ liệu hiện tại, và React cập nhật DOM thật khi dữ liệu đó thay đổi.

State và props khác nhau thế nào?

Props là đầu vào mà component cha truyền cho một component, và component đó xem chúng là chỉ đọc. State là dữ liệu do component sở hữu và thay đổi theo thời gian qua một setter, khiến React render lại component.

Hook trong React là gì?

Hook là các hàm có tên bắt đầu bằng use, cho phép function component dùng các tính năng của React: useState cho state, useEffect cho side effect, useContext cho context, useRef cho giá trị không kích hoạt render, và nhiều hook khác.

Virtual DOM là gì?

Đó là tên gọi cho bản mô tả giao diện mà React giữ trong bộ nhớ. Ở mỗi lần render, React tạo một bản mô tả mới, so sánh với bản trước và chỉ áp dụng các khác biệt vào DOM thật. Phép so sánh đó gọi là reconciliation.

Vì sao key quan trọng trong danh sách React?

Key cho React biết mục nào trong danh sách là mục nào giữa các lần render. Với key ổn định (như một id), React giữ state và node DOM của từng mục gắn đúng với dữ liệu khi các mục được thêm, xóa hoặc sắp xếp lại. Dùng chỉ số mảng làm key sẽ hỏng khi thứ tự thay đổi.

JSX là gì?

JSX là một phần mở rộng cú pháp cho phép bạn viết markup giống HTML trong JavaScript. Trình biên dịch biến mỗi thẻ thành một lời gọi hàm tạo React element, nên <h1>Hi</h1> trở thành một object thuần mô tả một thẻ h1.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu