Đi tới nội dung

Role chính của mình

Frontend Engineer

Xây giao diện web nhanh, dễ tiếp cận và bền vững khi sản phẩm lẫn team cùng lớn lên

Map này đi từ nền tảng web (HTML, CSS hiện đại, JavaScript, TypeScript, accessibility) đến React 19, Next.js App Router, testing, performance và kiến trúc frontend ở quy mô lớn. Dành cho bạn đã viết được vài trang React và muốn lên mức intermediate rồi advanced, hiểu vì sao chứ không chỉ biết dùng.

Bắt đầu từ chặng 01
chặng
10chặng
chủ đề
51chủ đề
tuần, học part-time
~34tuần, học part-time
  • Nền tảng
  • Cốt lõi
  • Nâng cao
  • TypeScriptTypeScript
  • ReactReact
  • Next.jsNext.js
  • TanStackTanStack Query
  • ViteVite
  • VitestVitest
  • PlaywrightPlaywright
  • TurborepoTurborepo

Góc nhìn của mình

Mình là frontend developer, đây là vai trò mình làm hằng ngày từ lúc thực tập ở AMIT Group năm 2022 đến giờ ở Estuary Solutions. Công việc của mình xoay quanh React, TypeScript, Next.js, Zustand, TanStack Query và một monorepo Bun + Turborepo chạy chính blog này. Trong lúc kèm hai bạn junior, mình nhận ra nhiều roadmap ngoài kia chỉ liệt kê công nghệ mà không nói thứ tự học hay vì sao thứ đó quan trọng. Map này là thứ tự mình sẽ đi nếu được bắt đầu lại hôm nay, cập nhật theo trạng thái của hệ sinh thái cuối 2026.

Điều mình ước biết sớm hơn

  • Lúc mới học mình nhảy vào framework quá sớm, nên nhiều bug thật ra là bug CSS, bug event loop hay bug HTML mà mình lại đi tìm trong React.
  • Server state và client state là hai thứ khác nhau; tách chúng ra sớm thì store toàn cục nhỏ đi rất nhiều và bug về dữ liệu cũ gần như biến mất.
  • Accessibility không phải việc làm thêm ở cuối sprint: dùng đúng element ngay từ đầu rẻ hơn rất nhiều so với vá ARIA sau này.
  • Điều mình ước biết sớm hơn là đo trước khi tối ưu: một lần profile trong DevTools đáng giá hơn cả buổi đoán xem component nào chậm.
Đã qua 0/10 chặng

Nền tảng01–03

  1. 01Chặng 01, 3 tuầnHTML ngữ nghĩa và CSS hiện đạiDựng được layout responsive, theo ngữ nghĩa, không cần thư viện UI và hiểu rõ cascade đang làm gì.
    Mình từng nghĩ CSS là phần dễ, đến khi gặp z-index chồng nhau và style bị ghi đè không rõ lý do. Hiểu cascade và layout cẩn thận một lần giúp mình tiết kiệm rất nhiều giờ debug về sau.

    Kiến thức cần nắm

    HTML ngữ nghĩa và form gốc
    Dùng landmark (header, nav, main, aside), heading theo thứ tự, button cho hành động và a cho điều hướng. Form gốc với label, fieldset, các type input và constraint validation (required, pattern, :user-invalid) cho bạn rất nhiều hành vi miễn phí. Element dialog với showModal() và popover attribute xử lý focus, top layer và Esc mà không cần tự viết.
    Layout: Flexbox, Grid, subgrid
    Flexbox cho bố cục một chiều, Grid cho hai chiều, subgrid để các card con thẳng hàng theo lưới của cha. Học kỹ min(), max(), clamp(), các đơn vị dvh/svh và logical properties (margin-inline, padding-block) để layout đúng cả với ngôn ngữ RTL.
    Container queries và :has()
    Container queries (@container, đơn vị cqi) cho phép component tự responsive theo kích thước vùng chứa thay vì viewport, rất hợp với design system. :has() là parent selector: style một card khi nó chứa ảnh, hoặc một form khi có input lỗi, giảm hẳn số class điều kiện viết bằng JavaScript.
    Cascade layers, nesting và specificity
    @layer cho phép khai báo thứ tự ưu tiên (reset, base, components, utilities) độc lập với specificity, nên CSS thư viện không còn đè lên CSS của bạn một cách khó hiểu. Kết hợp với CSS nesting gốc, :where() để giữ specificity bằng 0, và custom properties làm design token.
    View transitions và animation theo scroll
    View Transition API tạo hiệu ứng chuyển trạng thái hoặc chuyển trang bằng document.startViewTransition() và view-transition-name, với SPA đã có hỗ trợ trên các trình duyệt chính. Scroll-driven animations và anchor positioning giúp thay nhiều thư viện JavaScript; luôn kiểm tra trạng thái Baseline và tôn trọng prefers-reduced-motion.

    Nên làm

    • Kiểm tra trạng thái Baseline của một tính năng CSS trước khi dùng nó trong production.
    • Sắp xếp CSS theo @layer ngay từ đầu dự án thay vì tăng specificity để thắng.
    • Ưu tiên element gốc như button, dialog, details trước khi tự dựng bằng div.
    • Dùng custom properties cho màu, spacing, radius để đổi theme bằng một chỗ.

    Dễ sai

    • Dùng div có onClick thay cho button, làm mất focus, phím Enter/Space và vai trò với screen reader.
    • Viết media query theo từng thiết bị cụ thể thay vì để layout co giãn tự nhiên với Grid và clamp().
    • Lạm dụng !important để vá xung đột thay vì hiểu cascade.

    Project chốt chặng: Trang sản phẩm không dùng JavaScript

    Dựng một trang danh sách sản phẩm với card dùng container queries, bộ lọc dùng :has() và dialog chi tiết bằng element dialog. Đạt yêu cầu khi layout đúng từ 320px đến màn rộng, không có horizontal scroll và điều hướng được hoàn toàn bằng bàn phím.

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Cơ bảnVì sao nên thay <div onClick={save}> bằng <button onClick={save}>?
    2. Câu 2/4Cơ bảnVới @layer reset, base, components, utilities;, rule #app .card { padding: 2rem } nằm trong layer components và rule .p-4 { padding: 1rem } nằm trong layer utilities. Padding cuối cùng là gì?
    3. Câu 3/4Cơ bảnMột card component được dùng ở sidebar hẹp lẫn cột chính rộng, và cần chuyển sang layout ngang khi có đủ chỗ. Cách nào đúng?
    4. Câu 4/4Cơ bảnBạn cần một modal xác nhận có xử lý focus, đóng bằng Esc và hiển thị trên mọi thứ khác. Cách native nào phù hợp?
  2. 02Chặng 02, 4 tuầnJavaScript hiện đại và TypeScriptĐọc hiểu và viết được code bất đồng bộ đúng, mô hình hóa dữ liệu bằng type an toàn.
    Khi kèm junior, phần lớn bug mình gặp không nằm ở React mà ở closure cũ, promise không được await hoặc type any lan khắp nơi. Đầu tư vào phần này trả lãi trong mọi framework.

    Kiến thức cần nắm

    Event loop, promise và async
    Phân biệt macrotask, microtask và lúc trình duyệt render, để hiểu vì sao một vòng lặp nặng làm treo UI. Thành thạo async/await, Promise.all, Promise.allSettled, AbortController để hủy request và structuredClone để copy sâu dữ liệu.
    Module, closure và bất biến
    ES modules (import/export, dynamic import()) là nền tảng của code splitting và tree shaking. Hiểu closure và tham chiếu là chìa khóa để tránh stale state trong React; các method không đột biến như toSorted, toSpliced, with và Object.groupBy giúp code bất biến gọn hơn.
    TypeScript cho ứng dụng thật
    Bật strict, dùng discriminated union để mô hình trạng thái (idle, loading, success, error), satisfies để kiểm tra mà không làm rộng type, và generic có ràng buộc cho hàm tiện ích. Học utility types (Pick, Omit, Partial, ReturnType) và type narrowing thay vì ép kiểu bằng as.
    Validate ở biên hệ thống
    Type chỉ tồn tại lúc compile, dữ liệu từ API, URL hay localStorage cần validate lúc runtime bằng schema như Zod hoặc Valibot. Suy type từ schema (z.infer) để chỉ có một nguồn sự thật cho cả validate lẫn type.
    Tooling: Vite, ESLint, tsc
    Vite là lựa chọn mặc định cho SPA và thư viện; hiểu dev server dùng ESM gốc và build bằng bundler. Tách typecheck (tsc --noEmit) khỏi build, dùng typescript-eslint với rule type-aware. TypeScript đang được port sang native (Go) để typecheck nhanh hơn nhiều, nên theo dõi lộ trình này.

    Nên làm

    • Bật strict và noUncheckedIndexedAccess trong tsconfig cho mọi dự án mới.
    • Mô hình trạng thái bằng discriminated union thay vì nhiều boolean rời rạc.
    • Validate mọi dữ liệu đi vào từ bên ngoài bằng schema trước khi dùng.
    • Luôn truyền AbortSignal cho request có thể bị hủy khi người dùng rời trang.

    Dễ sai

    • Dùng any hoặc as để tắt lỗi thay vì sửa type, khiến bug lọt xuống runtime.
    • Quên await hoặc không bắt lỗi promise, dẫn đến unhandled rejection khó truy vết.
    • Tin rằng type từ API là đúng mà không validate dữ liệu thật.

    Project chốt chặng: Client API có type đầy đủ

    Viết một module fetch dữ liệu từ một API công khai với schema Zod, hỗ trợ hủy request, retry có giới hạn và trả về discriminated union. Đạt yêu cầu khi tsc strict không lỗi, không có any và có test cho trường hợp dữ liệu sai schema.

    • JavaScriptJavaScript
    • TypeScriptTypeScript
    • ViteVite

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Cơ bảnĐoạn code console.log('A'); setTimeout(() => console.log('B')); Promise.resolve().then(() => console.log('C')); console.log('D'); in ra theo thứ tự nào?
    2. Câu 2/4Cơ bảnĐã có type User trong TypeScript, vì sao vẫn cần parse response API bằng một Zod schema?
    3. Câu 3/4Cơ bảnVì sao nên mô hình state request bằng union { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: E } thay vì các boolean isLoading, isError cùng data?
    4. Câu 4/4Cơ bảnÔ tìm kiếm gọi fetch sau mỗi lần gõ, và đôi khi response cũ về sau response mới, ghi đè kết quả. Cách sửa nào đúng?
  3. 03Chặng 03, 2 tuầnAccessibility thực chiếnXây component dùng được bằng bàn phím và screen reader, đạt WCAG 2.2 mức AA.
    Mình học accessibility muộn và phải quay lại sửa nhiều modal, dropdown tự viết. Giờ mình coi nó là một phần của định nghĩa hoàn thành, không phải hạng mục riêng.

    Kiến thức cần nắm

    WCAG 2.2 và nguyên tắc POUR
    Nắm các tiêu chí hay gặp: độ tương phản màu, focus nhìn thấy được và không bị che (Focus Not Obscured), kích thước vùng chạm tối thiểu (Target Size), không phụ thuộc thao tác kéo thả. Nhiều thị trường đã đưa WCAG AA vào yêu cầu pháp lý, ví dụ European Accessibility Act có hiệu lực từ giữa 2025.
    Bàn phím và quản lý focus
    Thứ tự tab theo DOM, focus trap trong modal, trả focus về nút mở khi đóng, :focus-visible cho viền focus. Thuộc tính inert giúp vô hiệu hóa vùng nền khi mở overlay mà không cần thủ thuật.
    ARIA đúng cách
    Quy tắc đầu tiên của ARIA là không dùng ARIA nếu element gốc làm được. Khi tự dựng combobox, tabs, menu thì theo pattern trong ARIA Authoring Practices Guide, dùng aria-live cho thông báo động như toast hay kết quả tìm kiếm.
    Kiểm thử accessibility
    Kết hợp kiểm tra tự động (axe-core, eslint-plugin-jsx-a11y, Lighthouse) với kiểm tra thủ công bằng bàn phím và screen reader như NVDA hoặc VoiceOver. Công cụ tự động chỉ bắt được một phần lỗi, phần còn lại cần người dùng thử thật.

    Nên làm

    • Đặt label hiển thị hoặc accessible name cho mọi control tương tác.
    • Tự đi qua mỗi tính năng mới chỉ bằng bàn phím trước khi mở PR.
    • Dùng thư viện headless có sẵn a11y như Radix hoặc React Aria cho component phức tạp.
    • Chạy axe trong test component để bắt lỗi hồi quy sớm.

    Dễ sai

    • Xóa outline focus vì lý do thẩm mỹ mà không thay bằng kiểu focus khác.
    • Thêm role và aria-* bừa bãi khiến screen reader đọc sai còn tệ hơn không có.

    Project chốt chặng: Bộ component có thể truy cập

    Tự xây combobox có autocomplete và modal confirm theo ARIA APG. Đạt yêu cầu khi dùng được hoàn toàn bằng bàn phím, axe không báo lỗi và đọc đúng trên ít nhất một screen reader.

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Cơ bảnFirst rule of ARIA khuyên điều gì?
    2. Câu 2/4Cơ bảnTrang đã pass axe không còn lỗi nào. Vì sao vẫn chưa thể nói trang accessible?
    3. Câu 3/4Cơ bảnNgười dùng mở modal bằng nút Edit rồi nhấn Esc để đóng. Focus nên đi đâu?
    4. Câu 4/4Cơ bảnKết quả tìm kiếm cập nhật khi người dùng gõ, nhưng người dùng screen reader không biết số kết quả đã đổi. Cách sửa nào đúng?

Cốt lõi04–07

  1. 04Chặng 04, 4 tuầnMô hình tư duy của React 19Giải thích được React render khi nào, vì sao, và dùng đúng hook, Actions, Suspense cùng React Compiler.
    Mình đi từ React 18 sang 19 trong công việc, và thay đổi lớn nhất không phải API mới mà là cách nghĩ: ít useEffect hơn, ít memo thủ công hơn, để framework lo phần bất đồng bộ. Hiểu render là một hàm thuần giúp mọi thứ còn lại dễ hơn.

    Kiến thức cần nắm

    Render, commit và tính thuần
    Component là hàm thuần từ props và state ra UI; render có thể chạy nhiều lần, chỉ commit mới chạm DOM. Hiểu key, cách state gắn với vị trí trong cây, và vì sao StrictMode gọi render hai lần ở dev để lộ side effect sai chỗ.
    Hook và việc bạn có thể không cần Effect
    Phần lớn useEffect để đồng bộ state dẫn xuất là thừa: tính trực tiếp khi render hoặc xử lý trong event handler. useEffect chỉ dành cho đồng bộ với hệ thống bên ngoài; React 19.2 thêm useEffectEvent để tách phần giống event khỏi Effect, tránh kết nối lại không cần thiết.
    Actions và form trong React 19
    Actions là hàm async gắn với transition: useActionState quản lý kết quả và trạng thái pending, useOptimistic cho cập nhật lạc quan, useFormStatus đọc trạng thái form từ component con. Form có thể nhận action trực tiếp, và ref giờ là prop bình thường, không cần forwardRef.
    Suspense, use() và concurrent rendering
    Suspense khai báo vùng chờ dữ liệu hoặc code, use() đọc promise hoặc context ngay trong render. startTransition và useDeferredValue giữ UI phản hồi khi cập nhật nặng; component Activity trong React 19.2 giữ state của phần UI bị ẩn và hoãn cập nhật của nó.
    React Compiler
    React Compiler 1.0 đã ổn định, tự memo hóa component và giá trị nên useMemo, useCallback, memo thủ công phần lớn không còn cần. Compiler chỉ an toàn khi code tuân thủ Rules of React, nên bật bộ rule mới trong eslint-plugin-react-hooks trước, kể cả khi chưa bật compiler.

    Nên làm

    • Tính state dẫn xuất ngay khi render thay vì đồng bộ bằng useEffect.
    • Bật eslint-plugin-react-hooks với các rule của compiler và sửa hết cảnh báo.
    • Dùng useActionState và useOptimistic cho mutation thay vì tự quản lý cờ loading.
    • Đặt Suspense boundary theo vùng UI có ý nghĩa, không bọc toàn bộ trang.

    Dễ sai

    • Fetch dữ liệu trong useEffect không có cleanup, gây race condition khi đổi tham số nhanh.
    • Rải useMemo, useCallback khắp nơi mà không đo, làm code khó đọc mà không nhanh hơn.
    • Đột biến trực tiếp object trong state rồi thắc mắc vì sao UI không cập nhật.

    Project chốt chặng: Ứng dụng ghi chú với Actions

    Xây app ghi chú dùng form action, useActionState cho lỗi validate, useOptimistic khi thêm hoặc xóa và Suspense cho phần tải danh sách. Đạt yêu cầu khi bật React Compiler không có cảnh báo lint, không có useEffect nào dùng để đồng bộ state.

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Trung cấpTrong development với StrictMode, component render hai lần và Effect chạy mount, cleanup, mount. Vì sao React làm vậy?
    2. Câu 2/4Trung cấpComponent có const [fullName, setFullName] = useState('') và useEffect(() => setFullName(first + ' ' + last), [first, last]). Cách tốt hơn là gì?
    3. Câu 3/4Trung cấpCode items.push(newItem); setItems(items); không làm UI cập nhật. Vì sao và sửa thế nào?
    4. Câu 4/4Trung cấpNút Like cần hiển thị số like mới ngay lập tức và tự quay lại nếu server lỗi. Công cụ nào của React 19 phù hợp?
  2. 05Chặng 05, 3 tuầnClient state, server state và URL stateChọn đúng nơi lưu từng loại state và xây luồng dữ liệu có cache, refetch, optimistic update.
    Ở dự án đầu, mình nhét mọi thứ, kể cả dữ liệu từ API, vào store toàn cục và phải tự viết logic loading, cache, invalidate. Chuyển dữ liệu server sang TanStack Query và chỉ giữ UI state trong Zustand làm code gọn đi rõ rệt.

    Kiến thức cần nắm

    Phân loại state
    Tách state thành: local UI (useState), state chia sẻ phía client (Zustand hoặc context), server state (TanStack Query) và URL state (search params). Mỗi loại có vòng đời và nguồn sự thật khác nhau; nhầm loại là nguồn gốc của dữ liệu cũ và render thừa.
    TanStack Query v5
    Thiết kế query key có cấu trúc qua queryOptions factory, hiểu staleTime và gcTime, invalidateQueries sau mutation, và optimistic update qua onMutate hoặc biến của mutation. Dùng useSuspenseQuery khi muốn kết hợp với Suspense, và prefetch trên server rồi hydrate khi dùng với Next.js.
    Zustand cho client state
    Store nhỏ theo tính năng, đọc bằng selector để chỉ render lại khi phần cần thiết đổi, dùng useShallow khi selector trả object. Middleware persist cho dữ liệu lưu trên máy, devtools để debug; tránh biến Zustand thành bản sao cache của server.
    URL là state
    Bộ lọc, phân trang, tab đang chọn nên nằm trên URL để chia sẻ, back/forward và reload đều đúng. Thư viện như nuqs cho parse và serialize search params có type, hoạt động với Next.js App Router và React Router.
    Realtime và đồng bộ
    Với WebSocket hoặc Server-Sent Events, đẩy sự kiện vào cache của TanStack Query (setQueryData hoặc invalidate) thay vì giữ một store song song. Xử lý reconnect, thứ tự sự kiện và khi tab ẩn thì tạm dừng.

    Nên làm

    • Gom query key và queryFn vào queryOptions dùng chung cho cả prefetch lẫn component.
    • Đặt staleTime có chủ đích cho từng loại dữ liệu thay vì để mặc định bằng 0.
    • Đọc Zustand qua selector hẹp, không lấy cả store.
    • Đưa bộ lọc và phân trang lên URL ngay từ đầu.

    Dễ sai

    • Copy dữ liệu từ useQuery vào useState hoặc store, tạo hai nguồn sự thật lệch nhau.
    • Dùng query key dạng chuỗi tùy hứng khiến invalidate trượt hoặc xóa nhầm cache.

    Project chốt chặng: Dashboard quản lý đơn hàng

    Xây trang danh sách đơn hàng có bộ lọc trên URL, phân trang, cập nhật trạng thái đơn với optimistic update và nhận cập nhật realtime qua WebSocket giả lập. Đạt yêu cầu khi reload giữ nguyên bộ lọc, rollback đúng khi mutation lỗi và không có dữ liệu server nằm trong Zustand.

    • TanStackTanStack Query
    • Zustand
    • State

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Trung cấpVì sao const { data } = useQuery(todosQuery); const [todos, setTodos] = useState(data); là một lỗi thiết kế?
    2. Câu 2/4Trung cấpVới staleTime mặc định là 0, khi một component dùng cùng query mount lại trong lúc dữ liệu vẫn còn trong cache, TanStack Query làm gì?
    3. Câu 3/4Trung cấpTrang danh sách sản phẩm có filter và phân trang. Người dùng than gửi link cho người khác thì mất filter, và nút Back không quay lại filter trước. Cách sửa nào đúng?
    4. Câu 4/4Trung cấpBadge giỏ hàng trong header dùng const cart = useCartStore() và re-render mỗi khi bất kỳ phần nào của store đổi. Cách sửa nào đúng?
  3. 06Chặng 06, 4 tuầnNext.js App Router, RSC và formThiết kế ứng dụng Next.js 16 với ranh giới server/client rõ ràng, cache có chủ đích và form an toàn.
    Server Components buộc mình nghĩ lại chỗ dữ liệu được lấy và chỗ JavaScript thực sự cần chạy. Lúc đầu mình đánh dấu 'use client' quá rộng; đẩy ranh giới xuống các lá nhỏ làm bundle nhẹ hơn hẳn.

    Kiến thức cần nắm

    Server và Client Components
    Mặc định là Server Component: lấy dữ liệu trực tiếp, không gửi JavaScript xuống client. Chỉ đặt 'use client' ở các component lá cần state hoặc event, truyền Server Component vào Client Component qua children, và dùng server-only để chặn import nhầm code bí mật.
    Routing và streaming
    Nắm layout, loading.tsx, error.tsx, route group, parallel và intercepting routes. Streaming với Suspense cho phép phần tĩnh hiện ngay và phần chậm đổ về sau, cải thiện cả thời gian thấy nội dung lẫn trải nghiệm.
    Cache Components và 'use cache'
    Next.js 16 giới thiệu Cache Components (bật bằng cacheComponents): cache trở thành tùy chọn tường minh qua directive 'use cache', kết hợp cacheLife và cacheTag, phần động được stream bên trong Suspense. Hiểu khi nào invalidate theo tag, khi nào cần dữ liệu luôn mới, và rằng mặc định giờ là động chứ không còn cache ngầm.
    Server Functions và form
    Server Functions ('use server') nhận form action, kết hợp useActionState để trả lỗi validate, và revalidate dữ liệu sau khi ghi. Mỗi Server Function là một endpoint công khai: luôn validate input bằng schema và kiểm tra quyền ngay bên trong, không dựa vào việc UI đã ẩn nút.
    Validate form dùng chung client và server
    Một schema Zod dùng cho cả validate phía client (React Hook Form hoặc Conform) và phía server, nhờ chuẩn Standard Schema mà nhiều thư viện form đều nhận. Giữ progressive enhancement: form vẫn submit được khi JavaScript chưa tải xong.
    proxy.ts và biên mạng
    Ở Next.js 16, middleware.ts được đổi thành proxy.ts để làm rõ đây là lớp chặn request ở biên. Dùng nó cho redirect, rewrite, kiểm tra session nhẹ; phân quyền thật vẫn phải nằm ở data layer và Server Functions.

    Nên làm

    • Đẩy 'use client' xuống component lá nhỏ nhất có thể.
    • Tạo data access layer riêng kiểm tra auth trước khi trả dữ liệu cho component.
    • Validate input và kiểm tra quyền trong mọi Server Function.
    • Gắn cacheTag cho dữ liệu cache và invalidate theo tag sau khi ghi.

    Dễ sai

    • Truyền nguyên object từ database sang Client Component, làm lộ trường nhạy cảm vào HTML.
    • Chỉ kiểm tra quyền trong proxy hoặc layout rồi coi như mọi route con đã an toàn.
    • Không cập nhật kịp bản vá bảo mật của React và Next.js; lỗ hổng RCE trong React Server Components cuối 2025 là ví dụ rõ nhất.

    Project chốt chặng: Blog có trang quản trị

    Xây blog Next.js 16 với trang public được cache theo tag, trang quản trị có form tạo bài bằng Server Function và validate chung một schema. Đạt yêu cầu khi người không đăng nhập không gọi được Server Function, sửa bài thì trang public cập nhật ngay, và form chạy được khi tắt JavaScript.

    • Next.js 16
    • RSC
    • Forms

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Trung cấpNút gọi Server Function deleteUser chỉ hiện cho admin. Vì sao bên trong Server Function vẫn phải kiểm tra quyền?
    2. Câu 2/4Trung cấpVì sao nên đẩy 'use client' xuống component lá nhỏ nhất thay vì đặt ở layout?
    3. Câu 3/4Trung cấpMột Server Component render <ProfileCard user={await db.user.findUnique({ where: { id } })} />, trong đó ProfileCard là Client Component. Rủi ro là gì?
    4. Câu 4/4Trung cấpTrong Next.js 16 với Cache Components, dữ liệu sản phẩm được cache bằng 'use cache' và cacheTag('products'). Sau khi admin sửa sản phẩm trong một Server Function, cách nào đúng để trang hiển thị dữ liệu mới?
  4. 07Chặng 07, 3 tuầnChiến lược testing frontendXây bộ test nhanh, đáng tin, kiểm tra hành vi người dùng thay vì chi tiết cài đặt.
    Mình không phải chuyên gia QA, nhưng từng thấy test viết theo chi tiết cài đặt vỡ liên tục mỗi lần refactor. Test theo cách người dùng tương tác mới thực sự giúp mình tự tin khi sửa code.

    Kiến thức cần nắm

    Testing trophy và chọn tầng test
    Ưu tiên integration test ở mức component và tính năng, unit test cho logic thuần, vài E2E cho luồng quan trọng nhất, cộng static analysis (TypeScript, ESLint) làm nền. Mục tiêu là sự tự tin trên mỗi phút chạy CI, không phải độ phủ cao.
    Vitest và Browser Mode
    Vitest chạy nhanh, dùng chung cấu hình với Vite. Từ Vitest 4, Browser Mode đã ổn định, cho phép chạy test component trong trình duyệt thật qua Playwright thay vì jsdom, kèm visual regression bằng screenshot.
    Testing Library và MSW
    Query theo role và accessible name (getByRole) nên test vừa kiểm tra hành vi vừa gián tiếp kiểm tra a11y. Dùng user-event để mô phỏng tương tác thật và MSW để mock API ở tầng network, dùng chung handler cho test, Storybook và dev.
    Playwright cho E2E
    Locator theo role, auto-waiting, web-first assertion nên ít flaky hơn. Dùng fixtures, storageState để tái sử dụng đăng nhập, trace viewer để debug lỗi trên CI và chạy song song theo shard.
    Test Server Components và Actions
    Async Server Components còn khó unit test trực tiếp, nên tách logic dữ liệu ra hàm thuần để test riêng, còn luồng đầy đủ thì kiểm tra bằng E2E. Server Function nên được test như một API: input sai, thiếu quyền, thành công.

    Nên làm

    • Query phần tử theo role và label như người dùng thấy, tránh test id khi có lựa chọn tốt hơn.
    • Mock ở tầng network bằng MSW thay vì mock module fetch.
    • Bật trace on-first-retry cho Playwright trên CI.
    • Giữ test độc lập, không phụ thuộc thứ tự chạy hay dữ liệu chung.

    Dễ sai

    • Snapshot cả cây component lớn rồi cập nhật snapshot mỗi khi đỏ mà không đọc diff.
    • Dùng sleep cố định trong E2E thay vì chờ theo điều kiện, gây test chập chờn.

    Project chốt chặng: Bộ test cho dashboard đơn hàng

    Viết integration test bằng Vitest và Testing Library cho dashboard ở stage 05, mock API bằng MSW, và hai luồng E2E bằng Playwright chạy trên CI. Đạt yêu cầu khi toàn bộ chạy dưới vài phút, không flaky sau 10 lần chạy liên tiếp.

    • VitestVitest
    • PlaywrightPlaywright
    • Testing

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Trung cấpVì sao nên dùng getByRole('button', { name: 'Save' }) thay vì getByTestId('save-btn')?
    2. Câu 2/4Trung cấpVì sao mock API ở tầng network bằng MSW tốt hơn vi.mock('./api')?
    3. Câu 3/4Trung cấpTest Playwright await page.getByRole('button', { name: 'Save' }).click(); await page.waitForTimeout(2000); expect(await page.locator('.toast').textContent()).toBe('Saved'); thỉnh thoảng fail trên CI. Cách sửa nào đúng?
    4. Câu 4/4Trung cấpBạn cần test Server Function deletePost. Cách tiếp cận nào hợp lý nhất?

Nâng cao08–10

  1. 08Chặng 08, 3 tuầnPerformance và Core Web VitalsĐo, chẩn đoán và cải thiện LCP, INP, CLS dựa trên dữ liệu người dùng thật.
    Mình từng tối ưu theo cảm giác và tốn công vào những chỗ không đáng. Học đọc flame chart trong tab Performance là bước ngoặt: biết chính xác task nào đang chặn main thread.

    Kiến thức cần nắm

    Core Web Vitals và dữ liệu thật
    LCP đo tốc độ tải nội dung chính, INP đo độ phản hồi trong suốt phiên, CLS đo độ ổn định layout; ngưỡng tốt lần lượt khoảng 2.5s, 200ms và 0.1 ở phân vị 75. Phân biệt dữ liệu lab (Lighthouse) với dữ liệu field (CrUX, RUM qua thư viện web-vitals).
    Tối ưu INP
    INP gồm input delay, processing time và presentation delay. Chia nhỏ long task, nhường main thread bằng scheduler.yield() (có fallback), dùng startTransition cho cập nhật không khẩn cấp, và dùng Long Animation Frames API để tìm script gây chậm.
    Tối ưu LCP và CLS
    Ưu tiên tài nguyên LCP bằng fetchpriority=high, tránh lazy-load ảnh trên màn đầu, dùng định dạng AVIF hoặc WebP với srcset. Giữ chỗ cho ảnh và quảng cáo bằng width/height hoặc aspect-ratio, self-host font với font-display và size-adjust để giảm layout shift.
    Bundle và tải code
    Phân tích bundle, code split theo route và theo tương tác bằng dynamic import, loại thư viện nặng hoặc thay bằng API gốc. Server Components giúp giữ thư viện chỉ dùng để render khỏi bundle client.
    Render hiệu quả trong React
    Dùng React DevTools Profiler và Performance Tracks trong Chrome DevTools để thấy component nào render và tốn bao lâu. Ảo hóa danh sách dài (TanStack Virtual) và content-visibility: auto cho vùng ngoài màn hình.

    Nên làm

    • Thu thập Core Web Vitals từ người dùng thật và theo dõi theo route.
    • Đặt performance budget cho bundle và kiểm tra trong CI.
    • Profile trước khi tối ưu và đo lại sau khi sửa.
    • Test trên thiết bị tầm trung và mạng chậm, không chỉ trên máy dev.

    Dễ sai

    • Chạy theo điểm Lighthouse trong khi dữ liệu field vẫn kém.
    • Lazy-load luôn cả ảnh LCP khiến trang hiển thị chậm hơn.

    Project chốt chặng: Cải thiện một trang thật

    Chọn một trang trong dự án của bạn, đo baseline bằng web-vitals và Performance panel, rồi sửa ba vấn đề lớn nhất. Đạt yêu cầu khi có bảng trước/sau cho LCP, INP, CLS và kích thước bundle, kèm giải thích nguyên nhân từng cải thiện.

    • Core Web Vitals
    • INP
    • Performance

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Nâng caoLighthouse cho trang 100 điểm nhưng dữ liệu CrUX báo INP kém. Lý do hợp lý nhất là gì?
    2. Câu 2/4Nâng caoVì sao đặt loading="lazy" cho ảnh hero (phần tử LCP) làm LCP tệ hơn?
    3. Câu 3/4Nâng caoHandler click lọc 20.000 item trong một vòng lặp đồng bộ và INP là 600ms. Cách sửa nào đúng?
    4. Câu 4/4Nâng caoBanner quảng cáo tải muộn và đẩy nội dung xuống, CLS là 0.25. Cách sửa nào đúng?
  2. 09Chặng 09, 4 tuầnDesign system và kiến trúc quy mô lớnXây design system có token và component tái sử dụng, tổ chức monorepo với ranh giới module rõ ràng.
    Mình đã dùng Ant Design và Material UI nhiều năm, rồi mới hiểu khi tự vận hành monorepo Bun + Turborepo cho blog này: phần khó không phải viết component mà là giữ ranh giới giữa các package khi code lớn dần.

    Kiến thức cần nắm

    Design token
    Tổ chức token theo tầng: primitive (bảng màu), semantic (color.text.muted) và component. Định dạng của W3C Design Tokens Community Group cùng công cụ như Style Dictionary giúp sinh CSS variables, theme sáng tối và đồng bộ với Figma.
    Component headless và API component
    Dùng primitive headless (Radix, React Aria, Base UI) cho hành vi và a11y, tự lo phần style; mô hình shadcn/ui copy code vào dự án cho phép sở hữu hoàn toàn. Thiết kế API bằng composition (compound component, asChild) thay vì dồn hàng chục prop boolean.
    Tài liệu và kiểm thử component
    Storybook làm nơi phát triển và tài liệu sống, kết hợp interaction test, kiểm tra a11y và visual regression. Mỗi component có story cho các trạng thái: loading, lỗi, rỗng, nội dung dài.
    Monorepo với Turborepo
    Tách apps và packages (ui, config, utils), dùng workspace của pnpm hoặc Bun, cache task và chỉ chạy lại phần bị ảnh hưởng. Chia sẻ tsconfig và cấu hình ESLint qua package riêng để mọi app nhất quán.
    Ranh giới module
    Tổ chức code theo feature thay vì theo loại file, mỗi feature chỉ lộ API công khai qua entry point. Ép ranh giới bằng lint rule hoặc công cụ kiểm tra phụ thuộc, và cân nhắc micro-frontend chỉ khi có nhiều team cần deploy độc lập thật sự.

    Nên làm

    • Dùng token semantic trong component, không dùng mã màu trực tiếp.
    • Viết story cho mọi trạng thái của component trước khi dùng trong app.
    • Chặn import sâu vào nội bộ package hoặc feature khác bằng lint rule.
    • Version và ghi changelog cho package dùng chung bằng Changesets.

    Dễ sai

    • Xây design system quá sớm, trừu tượng hóa trước khi có ít nhất ba chỗ dùng thật.
    • Để package ui phụ thuộc ngược vào code của app, tạo vòng phụ thuộc.

    Project chốt chặng: Monorepo có package ui

    Dựng monorepo Turborepo gồm hai app và một package ui có token, theme sáng tối, năm component headless và Storybook. Đạt yêu cầu khi build có cache, đổi một token cập nhật cả hai app, và lint chặn được import sai ranh giới.

    • Design System
    • Monorepo
    • Architecture

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Nâng caoVì sao component nên dùng token color.text.muted thay vì gray-500?
    2. Câu 2/4Nâng caoComponent Card đã có showHeader, showFooter, compact, headerIcon, footerAlign... Vì sao nên chuyển sang compound component như <Card><Card.Header /><Card.Body /></Card>?
    3. Câu 3/4Nâng caoTrong monorepo, packages/ui import một hàm từ apps/web/lib/format. Vấn đề và cách sửa đúng là gì?
    4. Câu 4/4Nâng caoTeam có một sản phẩm với đúng một bảng dữ liệu và muốn xây package DataTable dùng chung với 30 prop. Lời khuyên hợp lý nhất?
  3. 10Chặng 10, 4 tuầnVận hành: observability, bảo mật, AIĐưa frontend lên production có giám sát lỗi, chống XSS bằng CSP và làm việc hiệu quả với AI mà vẫn kiểm soát chất lượng.
    Frontend dễ bị coi là chỉ hiển thị, nhưng code của mình chạy trên máy người dùng và xử lý cả token đăng nhập. Mình dùng AI coding assistant hằng ngày, và bài học lớn nhất là AI viết nhanh nhưng mình vẫn là người chịu trách nhiệm review.

    Kiến thức cần nắm

    Observability phía client
    Thu lỗi runtime kèm source map (Sentry hoặc tương đương), gắn release và user context, ghi Web Vitals theo route. OpenTelemetry cho trình duyệt cho phép nối trace từ click của người dùng xuyên qua API, dù phần browser vẫn còn đang phát triển nên cần đánh giá kỹ trước khi dùng.
    XSS và cách React bảo vệ
    React escape nội dung mặc định, nhưng dangerouslySetInnerHTML, href dạng javascript: và dữ liệu đưa vào script inline vẫn là lỗ hổng. Sanitize HTML bằng DOMPurify và không bao giờ tin dữ liệu chỉ vì nó đến từ API của mình.
    CSP và Trusted Types
    Content Security Policy dạng strict với nonce và strict-dynamic chặn phần lớn script bị chèn; Next.js hỗ trợ sinh nonce mỗi request. Trusted Types khóa các sink nguy hiểm như innerHTML, nên triển khai dần ở chế độ report-only trước.
    Auth và chuỗi cung ứng
    Lưu session trong cookie HttpOnly, Secure, SameSite thay vì localStorage, phòng CSRF cho request ghi. Khóa phiên bản dependency bằng lockfile, bật cảnh báo lỗ hổng và cập nhật bản vá framework nhanh, vì frontend hiện đại chạy cả code server.
    Làm việc cùng AI coding assistant
    Dùng AI để scaffold, viết test, refactor và giải thích code lạ, nhưng review từng diff như review code của đồng nghiệp. Cung cấp ngữ cảnh qua file hướng dẫn dự án và MCP server (docs, Figma, trình duyệt), và để test, typecheck, lint làm lưới an toàn cho code AI sinh ra.
    Xây UI cho tính năng AI
    Giao diện cho LLM cần streaming token, hủy request giữa chừng, hiển thị trạng thái công cụ đang chạy và xử lý lỗi hoặc kết quả sai. AI SDK của Vercel và các hook tương tự giúp quản lý luồng stream; render markdown từ model phải được sanitize như mọi dữ liệu không tin cậy.

    Nên làm

    • Upload source map lên công cụ giám sát lỗi nhưng không public chúng.
    • Triển khai CSP ở chế độ report-only rồi mới enforce.
    • Review mọi đoạn code AI sinh ra và yêu cầu có test đi kèm.
    • Theo dõi advisory bảo mật của framework và cập nhật bản vá trong vài ngày.

    Dễ sai

    • Lưu access token trong localStorage, nơi bất kỳ script XSS nào cũng đọc được.
    • Merge code AI sinh ra mà chưa hiểu nó, đặc biệt là code liên quan auth và dữ liệu.
    • Chỉ nhìn log server mà không biết người dùng đang gặp lỗi gì trên trình duyệt.

    Project chốt chặng: Đưa app lên production có giám sát

    Deploy ứng dụng ở stage 06 với error tracking có source map, CSP dùng nonce, cookie session an toàn và một tính năng chat có streaming từ LLM. Đạt yêu cầu khi lỗi được báo về kèm stack trace gốc, CSP không có vi phạm ở chế độ enforce, và output markdown của model được sanitize.

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Nâng caoVì sao không nên lưu access token trong localStorage?
    2. Câu 2/4Nâng caoVì sao nên triển khai CSP ở chế độ report-only trước khi enforce?
    3. Câu 3/4Nâng caoTính năng chat chuyển markdown từ model thành HTML rồi render bằng dangerouslySetInnerHTML. Cách làm đúng là gì?
    4. Câu 4/4Nâng caoBạn muốn stack trace dễ đọc trong Sentry mà không công khai source code. Cách nào đúng?

Những gì đang quan trọng lúc này và hướng đi trong vài năm tới.

Hiện tại

  • Server-first React đã thành mặc định

    Với Next.js 16 và React 19, Server Components, Server Functions và streaming là cách mặc định để xây app mới. Kỹ năng quan trọng là đặt ranh giới server/client đúng và hiểu cache tường minh, không chỉ biết viết hook.

  • Compiler thay cho memo thủ công

    React Compiler 1.0 ổn định và được khuyến nghị cho app mới, nên tối ưu render giờ chủ yếu là viết code tuân thủ Rules of React. Lint rule mới bắt lỗi từ sớm, kể cả khi chưa bật compiler.

  • Nền tảng web gánh nhiều việc hơn

    Container queries, :has(), cascade layers, view transitions, anchor positioning, popover và dialog đã đủ rộng để thay nhiều thư viện JavaScript. Ít dependency hơn nghĩa là bundle nhẹ hơn và ít lỗ hổng hơn.

  • AI là một phần của quy trình

    AI coding assistant đã có mặt trong công việc hằng ngày, từ scaffold đến viết test. Khác biệt nằm ở người biết review, đặt ngữ cảnh và giữ chất lượng bằng test, type và lint.

Sắp tới

  • Agent làm việc trong codebase

    AI agent sẽ nhận các task lớn hơn: tự chạy test, mở trình duyệt kiểm tra UI, sửa lỗi qua nhiều file. Hãy chuẩn bị bằng codebase có test tốt, script rõ ràng và tài liệu kiến trúc ngắn gọn để agent và người mới đều làm việc được.

  • Tooling native và nhanh hơn

    TypeScript đang được port sang native, các bundler và linter viết bằng Rust như Turbopack, Rolldown, Oxc ngày càng phổ biến. Nắm vững khái niệm (module graph, typecheck, transform) để chuyển công cụ dễ dàng thay vì gắn với cấu hình của một tool.

  • Frontend gánh thêm trách nhiệm server

    Khi Server Functions và edge runtime phổ biến, frontend engineer cần hiểu bảo mật server, phân quyền và observability đầu cuối. Học thêm căn bản về HTTP, cache CDN, database và OpenTelemetry sẽ là lợi thế.

  • Giao diện cho sản phẩm AI

    Ngày càng nhiều sản phẩm có UI streaming, generative UI và luồng có agent chạy nền. Kỹ năng thiết kế trạng thái chờ, hủy, xác nhận hành động và hiển thị độ tin cậy của kết quả sẽ thành yêu cầu cơ bản.

Đi hết roadmap, bạn sẽ

  • Bạn có thể dựng layout responsive, có thể truy cập, dùng CSS hiện đại mà không phụ thuộc thư viện UI.
  • Bạn có thể giải thích React render thế nào và viết component đúng Rules of React, sẵn sàng cho React Compiler.
  • Bạn có thể thiết kế luồng dữ liệu tách rõ server state, client state và URL state.
  • Bạn có thể xây ứng dụng Next.js 16 với Server Components, cache tường minh và Server Functions an toàn.
  • Bạn có thể viết bộ test nhiều tầng bằng Vitest, Testing Library và Playwright chạy ổn định trên CI.
  • Bạn có thể đo và cải thiện LCP, INP, CLS dựa trên dữ liệu người dùng thật.
  • Bạn có thể tổ chức monorepo có design system, giám sát lỗi production và áp dụng CSP chống XSS.

Bài viết liên quan

Tất cả bài viết

Mô hình tư duy đúng cho React Server Components: hai module graph, một ranh giới

"use client" không đánh dấu loại component mà đánh dấu điểm bắt đầu của client graph. Bài này dựng lại mô hình tư duy cho RSC trong Next.js 16: thứ gì đi qua ranh giới, data fetching đặt ở đâu, waterfall, streaming và những lỗi khiến bundle phình hoặc lộ dữ liệu.

Frontend & giao diện19 phút

Câu hỏi phỏng vấn Frontend Engineer

Xem cả 244 câu

Các role khác

Roadmap

Thiết kế, xây dựng và vận hành API, dữ liệu và service phía sau mỗi sản phẩm

10 chặng, 52 chủ đề, ~37 tuần

Xây app iOS và Android mượt, chạy được khi mất mạng và phát hành an toàn

10 chặng, 50 chủ đề, ~34 tuần

Tìm rủi ro trước người dùng và biến chất lượng thành thứ cả team đo được

10 chặng, 50 chủ đề, ~36 tuần

Xây hạ tầng, pipeline và nền tảng để team ship nhanh, chạy ổn định, an toàn

10 chặng, 56 chủ đề, ~42 tuần

Xây sản phẩm trên nền LLM: từ gọi API, RAG, agent đến eval và vận hành production

10 chặng, 58 chủ đề, ~31 tuần