Bộ này được cập nhật ngày 05/11/2026 theo JD Frontend ReactJS bạn gửi. Mục tiêu không phải học thuộc đáp án, mà là đủ chắc để giải thích quyết định kỹ thuật và viết được một phần tính năng ngay trong buổi phỏng vấn.

0. Đọc JD như một kỹ sư

JD này cần một người có thể nhận đặc tả, chia giao diện thành component, gọi API, xử lý state, tự kiểm tra trên nhiều màn hình rồi đưa code qua review. React chỉ là một phần. Người phỏng vấn có thể đi từ JavaScript, browser và CSS trước khi hỏi hook.

Tự chấm theo ba mức:

  • Biết: giải thích được khái niệm bằng lời của mình.
  • Làm được: viết ví dụ nhỏ không nhìn tài liệu.
  • Đã va chạm: kể được một lỗi, cách tìm nguyên nhân và đánh đổi của giải pháp.

Nếu thiếu mức thứ ba, đừng dựng chuyện. Hãy nói: “Em chưa gặp đúng tình huống đó trong production; cách em sẽ kiểm tra là…”. Một câu trung thực kèm quy trình suy luận tốt hơn một câu chuyện không đứng vững khi bị hỏi sâu.

1. HTML và nền tảng trình duyệt

Semantic HTML

HTML tốt cho browser biết mỗi vùng có vai trò gì trước khi CSS và JavaScript chạy. Dùng button cho hành động, a cho điều hướng, label cho input, và giữ thứ tự heading có nghĩa. Không dùng div có onClick để giả làm button: bạn sẽ phải tự thêm keyboard interaction, focus và semantics mà phần tử gốc đã có sẵn.

tsx
export function SearchForm() {
  return (
    <form role="search">
      <label htmlFor="keyword">Từ khóa</label>
      <input id="keyword" name="q" type="search" autoComplete="off" />
      <button type="submit">Tìm</button>
    </form>
  );
}

Những ý nên nói được:

  • DOM là cây node; event thường đi qua capture, target rồi bubble.
  • preventDefault() chặn hành vi mặc định; stopPropagation() chặn event đi tiếp. Hai việc khác nhau.
  • defer tải script song song và chạy sau khi HTML parse; async chạy ngay khi tải xong, không giữ thứ tự.
  • Form native có keyboard behavior, submit bằng Enter và validation cơ bản miễn phí.
  • Accessibility không chỉ là thêm ARIA. “No ARIA is better than bad ARIA”; ưu tiên HTML đúng trước.

Câu hỏi nói miệng: Vì sao một button cần khai báo type="button" khi nằm trong form? Vì mặc định nó có thể là submit button và vô tình gửi form.

Bài tập 20 phút: Viết modal xác nhận xóa. Tab không được rơi ra sau modal, Escape đóng modal, focus quay lại nút mở. Sau đó giải thích khi nào nên dùng native <dialog>.

2. CSS và responsive không dựa vào “thiết bị chuẩn”

Responsive là thiết kế phòng thủ: layout phải chịu được chuỗi dài, zoom, font lớn, màn hình hẹp và dữ liệu không đều. Bắt đầu mobile-first, dùng Grid/Flexbox, kích thước tương đối và breakpoint tại nơi nội dung bắt đầu vỡ—không theo tên một mẫu điện thoại.

css
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  gap: clamp(0.75rem, 2vw, 1.5rem);
}
 
.card-grid > * {
  min-width: 0;
}
 
img,
video {
  max-width: 100%;
  height: auto;
}

min-width: 0 đáng nhớ khi dùng Grid/Flexbox. Mặc định một item có thể giữ min-content width của code, URL dài hoặc bảng và đẩy cả trang tràn ngang. Đây chính là kiểu lỗi phải kiểm tra ở 320–390px, không chỉ kéo cửa sổ “trông có vẻ ổn”.

Best practice:

  • Dùng rem cho typography/spacing cần tôn trọng cỡ chữ người dùng.
  • Dùng clamp() cho kích thước fluid nhưng có trần và sàn.
  • Đặt chiều rộng đọc bài khoảng 60–75 ký tự một dòng.
  • Với component được tái sử dụng ở nhiều vùng, cân nhắc container query thay vì chỉ viewport query.
  • Test keyboard, zoom 200%, dark mode, reduced motion và chuỗi không có khoảng trắng.

Bài tập 30 phút: Dựng danh sách sản phẩm gồm ảnh, tên, giá và nút. Ở 360px là một cột; khi đủ chỗ tự tăng số cột; tên dài ba dòng không làm nút lệch. Không viết media query theo iPhone/iPad.

Đọc thêm về responsive design trên MDN (mở tab mới).

3. JavaScript: phần dễ lộ nhất khi hỏi sâu

Scope, closure và event loop

let/const có block scope; var có function scope và được hoist theo cách dễ tạo lỗi. Closure là khi một function giữ được tham chiếu tới lexical environment nơi nó được tạo. Hook, event handler, debounce và memoization đều dựa vào nền tảng này.

ts
function createRequestGate() {
  let latest = 0;
 
  return async function run<T>(task: () => Promise<T>) {
    const requestId = ++latest;
    const result = await task();
    return requestId === latest ? result : null;
  };
}

Event loop phân biệt call stack, task và microtask. Callback của Promise đi vào microtask queue, thường chạy trước timer task sau khi stack rỗng.

js
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
// A, D, C, B

Giá trị, tham chiếu và bất biến

Object/array được truyền bằng giá trị của tham chiếu. Spread chỉ sao chép nông. Khi cập nhật state lồng nhau, bạn phải tạo object mới ở mọi tầng đã đổi hoặc chọn cấu trúc state phẳng hơn.

ts
const nextUser = {
  ...user,
  profile: { ...user.profile, displayName: "Linh" },
};

Promise và xử lý lỗi

Promise.all fail-fast; Promise.allSettled phù hợp khi muốn biết kết quả từng việc độc lập. try/catch chỉ bắt được Promise nếu bạn await nó hoặc return đúng chain. Đừng trộn await và .then() trong cùng luồng nếu không có lý do.

Câu hỏi thường gặp: debounce và throttle khác gì? Debounce chờ chuỗi sự kiện im trong một khoảng rồi mới chạy; throttle giới hạn tần suất chạy. Search suggestion thường debounce, scroll/resize telemetry thường throttle hoặc gom theo animation frame.

Bài tập 40 phút: Viết debounce(fn, delay) có cancel(), giữ đúng this và arguments. Viết test fake timer cho ba lần gọi liên tiếp.

4. TypeScript dùng để mô hình hóa, không phải trang trí

Bật strict. Nhận dữ liệu bên ngoài là unknown, kiểm tra ở boundary rồi mới đưa kiểu đã tin cậy vào phần còn lại của app. as User không validate dữ liệu lúc chạy.

ts
type User = { id: string; name: string };
 
function isUser(value: unknown): value is User {
  if (typeof value !== "object" || value === null) return false;
  const candidate = value as Record<string, unknown>;
  return typeof candidate.id === "string" && typeof candidate.name === "string";
}

Discriminated union biểu diễn state UI tốt hơn một cụm boolean có thể mâu thuẫn:

ts
type AsyncState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };
 
function assertNever(value: never): never {
  throw new Error(`Unhandled state: ${JSON.stringify(value)}`);
}

Best practice:

  • Ưu tiên inference; khai báo kiểu ở public boundary và nơi inference không diễn đạt ý định.
  • Dùng satisfies để kiểm tra shape mà vẫn giữ literal type.
  • Dùng generic khi quan hệ giữa input và output cần được giữ, không dùng chỉ để “trông nâng cao”.
  • Tránh any; nếu buộc phải dùng ở migration boundary, giới hạn phạm vi và để lại đường xóa.
  • Phân biệt optional (name?: string) với có mặt nhưng có thể undefined (name: string | undefined).

Bài tập 45 phút: Viết groupBy<T, K extends PropertyKey> trả về Record<K, T[]>. Sau đó thiết kế kiểu props cho button: khi render thành link phải có href, khi là button thì không được có href.

Ôn lại narrowing trong TypeScript Handbook (mở tab mới).

5. React: render, state và effect

Mental model trước API

Một lần render là một snapshot. Props, state và handler trong lần render đó nhìn thấy giá trị của snapshot đó. Gọi setter yêu cầu React lên lịch render mới; nó không sửa biến state đang nằm trong function hiện tại.

Giữ state tối thiểu. Nếu một giá trị có thể tính từ props/state trong render, đừng lưu thêm một bản sao:

tsx
function CartSummary({ items }: { items: Array<{ price: number; quantity: number }> }) {
  const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  return <strong>{total.toLocaleString("vi-VN")} đ</strong>;
}

useEffect dùng để đồng bộ với hệ thống bên ngoài: network, subscription, DOM API, timer. Không dùng effect để tính dữ liệu thuần từ props hoặc để mô phỏng event handler.

tsx
useEffect(() => {
  const controller = new AbortController();
 
  void loadUser(userId, controller.signal).then(setUser).catch((error) => {
    if (error instanceof DOMException && error.name === "AbortError") return;
    setError("Không tải được người dùng");
  });
 
  return () => controller.abort();
}, [userId]);

Component boundary và composition

Tách component theo trách nhiệm và nhịp thay đổi, không theo số dòng. Composition thường linh hoạt hơn một component nhận hai mươi prop cấu hình. Đặt state gần nơi dùng nhất; nâng lên common parent khi hai nhánh cần đồng bộ.

React hiện đại cần biết đến đâu

Với React 19, nên nhận diện Actions, useActionState, useOptimistic và API use, nhưng đừng lôi chúng vào mọi app client. Trong phỏng vấn, nền tảng render/state/effect và khả năng debug vẫn quan trọng hơn việc đọc tên API mới.

useMemo và useCallback là công cụ tối ưu, không phải mặc định. Chỉ dùng khi đã có lý do: computation thực sự đắt, identity ổn định là một phần của API, hoặc profiler cho thấy child re-render đáng kể.

Câu hỏi nói miệng: Tại sao không dùng index làm key cho list có thể reorder? Vì key là identity để React ghép state với item. Khi thứ tự đổi, index giữ vị trí chứ không giữ thực thể, state có thể “nhảy” sang item khác.

Bài tập 60 phút: Viết autocomplete gồm loading, empty, error, abort request cũ và keyboard navigation. Không dùng thư viện data fetching ở vòng đầu. Sau đó nói cách bạn thay bằng TanStack Query.

React có chương Managing State rất đáng ôn lại (mở tab mới).

6. Chọn state management theo chủ sở hữu dữ liệu

Đừng bắt đầu bằng tên thư viện. Hỏi bốn câu:

  1. Ai là chủ dữ liệu: server hay client?
  2. Có bao nhiêu component đọc/ghi?
  3. Dữ liệu đổi thường xuyên tới đâu?
  4. Có cần cache, retry, dedupe hoặc optimistic update không?

Gợi ý lựa chọn:

  • State cục bộ: useState/useReducer.
  • Giá trị xuyên cây nhưng ít đổi: Context.
  • Client state dùng ở nhiều nhánh và cần selector: Zustand hoặc Redux Toolkit.
  • Server state: TanStack Query/SWR hoặc cơ chế data fetching của framework.

Redux không “tệ”; nó hữu ích khi team cần convention rõ, action log, middleware và luồng cập nhật dự đoán được. Zustand nhẹ hơn nhưng team phải tự thống nhất boundary, naming và cách tránh store toàn cục phình ra. Context không phải store tối ưu cho state đổi liên tục vì mọi consumer của context value có thể render lại.

Bài tập 30 phút: Cho bốn dữ liệu—theme, form checkout, danh sách sản phẩm từ API, điểm realtime của game—hãy chọn nơi lưu và giải thích. Không có đáp án chỉ dựa vào tên thư viện.

7. REST API và trạng thái bất đồng bộ

REST thường tổ chức resource qua URL và HTTP method. Nắm ý nghĩa của GET, POST, PUT, PATCH, DELETE; status 2xx/4xx/5xx; idempotency; query parameters; auth token; CORS; pagination.

fetch chỉ reject với lỗi network hoặc abort, không reject chỉ vì server trả 404/500. Luôn kiểm tra response.ok hoặc status.

ts
type ApiError = { message?: string };
 
async function requestJson<T>(url: string, init?: RequestInit): Promise<T> {
  const response = await fetch(url, {
    ...init,
    headers: { Accept: "application/json", ...init?.headers },
  });
 
  if (!response.ok) {
    const body = (await response.json().catch(() => null)) as ApiError | null;
    throw new Error(body?.message ?? `HTTP ${response.status}`);
  }
 
  return (await response.json()) as T;
}

Trong code thực tế, generic T ở đây vẫn là niềm tin, không phải runtime validation. Với boundary quan trọng, dùng schema validator hoặc type guard.

UI cần phân biệt initial loading, background refetch, empty, validation error và server error. Disable submit trong lúc gửi chưa đủ: hãy chống double submit ở cả client và backend nếu action không idempotent.

Bài tập 60 phút: Làm trang danh sách user có search, pagination, retry và cancel request. Vẽ state machine trước khi code. Viết test cho 200, 404, 500, JSON hỏng và request bị abort.

MDN giải thích rõ việc kiểm tra Response.ok và hủy request (mở tab mới).

8. Performance: đo trước, tối ưu đúng tầng

Ba Core Web Vitals cần nhớ:

  • LCP: nội dung lớn nhất xuất hiện; mục tiêu tốt không quá 2,5 giây.
  • INP: độ trễ tương tác; mục tiêu tốt không quá 200 ms.
  • CLS: mức xê dịch layout; mục tiêu tốt không quá 0,1.

Các ngưỡng được đánh giá ở percentile 75 của lượt truy cập thực. Lighthouse là lab data; Search Console/CrUX là field data. Một điểm Lighthouse đẹp không thay thế dữ liệu người dùng.

Quy trình:

  1. Xác định route và thiết bị chậm.
  2. Đo bằng Performance/Network panel và React Profiler.
  3. Tìm bottleneck: network, main thread, render hay layout.
  4. Thay đổi một giả thuyết có thể kiểm chứng.
  5. Đo lại và theo dõi regression.

Các đòn bẩy thường có giá trị:

  • Giảm JavaScript gửi xuống client; lazy-load tính năng nặng.
  • Cho LCP asset tải sớm, có kích thước và không bị script che.
  • Đặt width/height hoặc aspect ratio cho media để tránh CLS.
  • Virtualize danh sách thật sự dài, không áp dụng cho 30 item.
  • Tránh task dài; chia việc và giảm tính toán đồng bộ trong event handler.
  • Cache dữ liệu và asset đúng semantics, không cache mù.

Bài tập 45 phút: Lấy một trang đang có, ghi lại LCP/INP/CLS và waterfall. Đưa ra ba giả thuyết, chọn một thay đổi, rồi lưu số trước/sau. Nếu chưa đo, không được viết “tăng 40%”.

Tài liệu web.dev về LCP (mở tab mới).

9. Next.js, Git, test và code review

Next.js cần đủ dùng cho JD ưu tiên

Phân biệt Server Component và Client Component. Client boundary cần khi có state, event hoặc browser API; đừng thêm "use client" ở root chỉ vì một nút nhỏ. App Router dùng route theo file. Từ Next.js 15, dynamic params là Promise; static export cần biết trước dynamic params khi build.

Các điểm nên giải thích được:

  • SSR, SSG/static export và CSR khác nhau ở nơi/thời điểm render.
  • Metadata/canonical/sitemap phục vụ crawl và chia sẻ.
  • next/font self-host font sau build, tránh request Google Fonts lúc runtime.
  • next/image mặc định cần optimization server; static host phải cấu hình phù hợp.
  • Đặt Server/Client boundary sai có thể gửi nhiều JavaScript hơn cần thiết.

Git và review

Một commit tốt có một mục đích, message nói được “vì sao”. Trước khi mở PR: tự đọc diff, chạy test, xóa log, ghi cách kiểm tra và nêu rõ vùng rủi ro. Khi review, phân biệt blocker với suggestion; tập trung correctness, maintainability, security và test thay vì sở thích cá nhân.

Biết các thao tác: branch, commit, rebase/merge, resolve conflict, revert, cherry-pick, stash và đọc git log/git blame. Với lịch sử đã chia sẻ, ưu tiên revert thay vì rewrite tùy tiện.

Testing pyramid vừa đủ

  • Unit test cho hàm thuần và logic nhiều nhánh.
  • Component/integration test cho hành vi người dùng và boundary giữa các phần.
  • E2E cho luồng quan trọng, không dùng để test mọi permutation.

Test hành vi quan sát được thay vì implementation detail. Ví dụ, tìm button theo role/name và click như người dùng, không assert tên state nội bộ.

Bài tập 45 phút: Nhận một PR có fetch trong useEffect, thiếu abort, dùng index key và không có error state. Viết review thành bốn comment: mức độ, hậu quả, đề xuất và test cần thêm.

10. Làm việc với BA, Backend, Tester và khách hàng Nhật

Phần này thường quyết định bạn có phù hợp dự án dài hạn hay không. Khi nhận ticket, đừng chỉ hỏi “bao giờ xong”. Hãy làm rõ acceptance criteria, trường hợp lỗi, quyền truy cập, dữ liệu rỗng, timezone, format số/ngày, browser support và API contract.

Một update tốt ngắn nhưng có cấu trúc:

text
Done: màn danh sách và loading/empty state.
In progress: nối API pagination.
Blocked: response chưa có totalCount; cần BE xác nhận trước 15:00.
Risk: nếu totalCount không có, UI chỉ hỗ trợ next/previous.

Với tài liệu tiếng Anh hoặc khách hàng Nhật, xác nhận lại bằng văn bản: “My understanding is…”, kèm ví dụ input/output và ảnh khi cần. Đừng im lặng đoán requirement. Báo blocker sớm cùng một phương án tạm thời.

Câu hỏi hành vi: “Kể về một lần không đồng ý với BA/Tester.” Trả lời theo STAR nhưng tập trung bằng chứng: bối cảnh, trách nhiệm, hành động cụ thể, kết quả và điều rút ra. Không biến đồng đội thành người sai để mình thành người hùng.

Bài tập 20 phút: Viết một comment tiếng Anh hỏi lại API khi đặc tả nói field date nhưng không ghi timezone. Đưa ra hai ví dụ để backend trả lời được bằng một quyết định rõ ràng.

11. Bộ câu hỏi tự kiểm tra

Hãy trả lời mỗi câu trong 60–90 giây:

  1. React render lại component khi nào? Render lại có đồng nghĩa DOM đổi không?
  2. useEffect cleanup chạy lúc nào? Lỗi stale closure xuất hiện ra sao?
  3. Controlled và uncontrolled input khác nhau thế nào?
  4. Khi nào Context đủ dùng, khi nào cần store có selector?
  5. Promise.all và allSettled khác nhau trong màn dashboard thế nào?
  6. Vì sao fetch 500 không tự nhảy vào catch?
  7. PUT và PATCH khác gì? Idempotency ảnh hưởng retry ra sao?
  8. Type guard khác type assertion ở runtime thế nào?
  9. Vì sao Grid/Flex item đôi khi gây horizontal overflow?
  10. Bạn sẽ đo và tối ưu một trang React chậm theo thứ tự nào?
  11. Khi nào memoization làm code tệ hơn?
  12. SSG, SSR và CSR phù hợp với ba loại trang nào?
  13. Bạn tổ chức branch và PR ra sao khi feature kéo dài nhiều ngày?
  14. Nếu API contract thay đổi sát deadline, bạn phối hợp thế nào?
  15. Bạn làm gì khi không tái hiện được bug Tester báo?

Nếu câu trả lời chỉ là định nghĩa, hãy thêm một ví dụ và một trade-off. Nếu ví dụ là dự án thật, chuẩn bị để người phỏng vấn hỏi sâu đến dữ liệu, cách đo và phần bạn trực tiếp làm.

12. Bài thực hành tổng hợp

Xây một “Issue Board” nhỏ trong 3–4 giờ:

  • React + TypeScript strict.
  • Lấy danh sách issue từ REST API giả.
  • Search có debounce, filter status, pagination.
  • Loading, empty, error, retry và abort request cũ.
  • URL lưu filter để reload không mất trạng thái.
  • Responsive từ 360px, dùng semantic HTML và keyboard được.
  • Có unit test cho filter và integration test cho một luồng fetch lỗi → retry → thành công.
  • README ghi quyết định, đánh đổi, cách chạy và phần sẽ làm tiếp nếu có thêm thời gian.

Sau khi xong, tự review bằng ba câu:

  1. Nếu API trả dữ liệu sai shape, app hỏng ở đâu?
  2. Nếu danh sách tăng từ 20 lên 20.000 item, kiến trúc nào cần đổi?
  3. Nếu BA yêu cầu thêm sort và lưu view theo user, state nào chuyển chủ sở hữu?

13. Lịch ôn 7 ngày

NgàyTrọng tâmSản phẩm cuối ngày
1HTML, CSS, responsive, accessibilityMột trang responsive không tràn ngang, keyboard dùng được
2JavaScript, event loop, PromiseDebounce có test và năm câu giải thích bằng lời
3TypeScript strictAPI boundary có validation và union state
4React render/state/effectAutocomplete có abort và error state
5REST, state management, performanceIssue list có cache/filter và bản đo performance
6Next.js, Git, testing, reviewMột PR hoàn chỉnh kèm self-review
7Mock interview và bài tổng hợp45 phút mock + danh sách chỗ còn ấp úng

Ngày cuối, đừng nhồi thêm API mới. Ngủ đủ, kiểm tra camera/mic, mở sẵn CV và JD, chuẩn bị ba câu hỏi cho nhà tuyển dụng: cấu trúc team, quy trình nhận/review đặc tả, và tiêu chí đánh giá trong hai tháng đầu.