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.
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.defertải script song song và chạy sau khi HTML parse;asyncchạ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.
.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
remcho 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.
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.
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
// A, D, C, BGiá 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.
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.
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:
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:
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.
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:
- Ai là chủ dữ liệu: server hay client?
- Có bao nhiêu component đọc/ghi?
- Dữ liệu đổi thường xuyên tới đâu?
- 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.
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:
- Xác định route và thiết bị chậm.
- Đo bằng Performance/Network panel và React Profiler.
- Tìm bottleneck: network, main thread, render hay layout.
- Thay đổi một giả thuyết có thể kiểm chứng.
- Đ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/heighthoặ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/fontself-host font sau build, tránh request Google Fonts lúc runtime.next/imagemặ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:
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:
- React render lại component khi nào? Render lại có đồng nghĩa DOM đổi không?
useEffectcleanup chạy lúc nào? Lỗi stale closure xuất hiện ra sao?- Controlled và uncontrolled input khác nhau thế nào?
- Khi nào Context đủ dùng, khi nào cần store có selector?
Promise.allvàallSettledkhác nhau trong màn dashboard thế nào?- Vì sao
fetch500 không tự nhảy vàocatch? - PUT và PATCH khác gì? Idempotency ảnh hưởng retry ra sao?
- Type guard khác type assertion ở runtime thế nào?
- Vì sao Grid/Flex item đôi khi gây horizontal overflow?
- Bạn sẽ đo và tối ưu một trang React chậm theo thứ tự nào?
- Khi nào memoization làm code tệ hơn?
- SSG, SSR và CSR phù hợp với ba loại trang nào?
- Bạn tổ chức branch và PR ra sao khi feature kéo dài nhiều ngày?
- Nếu API contract thay đổi sát deadline, bạn phối hợp thế nào?
- 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:
- Nếu API trả dữ liệu sai shape, app hỏng ở đâu?
- Nếu danh sách tăng từ 20 lên 20.000 item, kiến trúc nào cần đổi?
- 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ày | Trọng tâm | Sản phẩm cuối ngày |
|---|---|---|
| 1 | HTML, CSS, responsive, accessibility | Một trang responsive không tràn ngang, keyboard dùng được |
| 2 | JavaScript, event loop, Promise | Debounce có test và năm câu giải thích bằng lời |
| 3 | TypeScript strict | API boundary có validation và union state |
| 4 | React render/state/effect | Autocomplete có abort và error state |
| 5 | REST, state management, performance | Issue list có cache/filter và bản đo performance |
| 6 | Next.js, Git, testing, review | Một PR hoàn chỉnh kèm self-review |
| 7 | Mock interview và bài tổng hợp | 45 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.