Base.vn Prep

2026-05-19 · Middle-level

FRONTEND

Frontend Deep Dive

React Hooks · TanStack Query · Next.js patterns · Performance. Stack focus: Next.js 16 + React 19 + MUI v7 + Tailwind v4.

1 · React Hooks Essentials

useState

Local state. Functional update setX(prev => prev+1) khi depend giá trị cũ.

useEffect

Side effect sau render. Cleanup return function. Empty deps = mount-only.

useMemo

Cache giá trị tính nặng. KHÔNG dùng default — chỉ khi profiler thấy slow.

useCallback

Cache function ref. Chỉ cần khi pass xuống React.memo child.

useRef

Mutable value KHÔNG re-render. DOM ref, timer id, prev value.

Custom hook

Tên use*. Encapsulate logic + state. Reusable.

// Custom hook
function useDebounce<T>(value: T, delay = 300): T {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer); // cleanup
  }, [value, delay]);
  return debounced;
}

Pitfall: useEffect deps thiếu → stale closure. Object/array literal trong deps → effect mỗi render. Cleanup quên → memory leak. useMemo mọi nơi → tăng RAM.

2 · TanStack Query Patterns

Vì sao: Cache + dedup + background refetch + staleTime + optimistic. Tự handle loading/error.

// Query key factory — tránh typo + dễ invalidate
export const taskKeys = {
  all: ['tasks'] as const,
  list: (filters: TaskFilter) => [...taskKeys.all, 'list', filters] as const,
  detail: (id: string) => [...taskKeys.all, 'detail', id] as const,
};

export function useTasks(filters: TaskFilter) {
  return useQuery({
    queryKey: taskKeys.list(filters),
    queryFn: () => api.tasks.list(filters),
    staleTime: 1000 * 60, // 1 min
  });
}

export function useCreateTask() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: api.tasks.create,
    onSuccess: () => qc.invalidateQueries({ queryKey: taskKeys.all }),
  });
}

staleTime

Bao lâu data còn fresh. Trong window không refetch.

gcTime

Bao lâu giữ memory sau khi không dùng. Default 5m.

Optimistic

onMutate set trước, rollback khi error.

Pitfall: queryKey không stable (inline object) → refetch loạn. Quên invalidate sau mutation → stale UI. enabled:false nhưng vẫn render → conditional query phải chuẩn.

3 · Next.js Patterns (App Router)

Server Component (RSC)

Default trong app/. Render server, không state/event, không ship JS. Fetch DB trực tiếp.

Client Component

"use client" directive. Có state/effect. Hydrate trên client.

Server Action

"use server". Form submit trực tiếp tới function server — không cần API route.

Streaming + Suspense

loading.tsx cho route. <Suspense> stream phần data chậm.

// app/tasks/page.tsx — Server Component
export default async function TasksPage() {
  const tasks = await db.task.findMany(); // fetch trực tiếp DB
  return <TaskList tasks={tasks} />; // Client Component
}

// app/tasks/TaskList.tsx
"use client";
import { useState } from 'react';
export function TaskList({ tasks }) {
  const [filter, setFilter] = useState('all');
  return /* interactive UI */;
}

Caching layers: Request memoization (1 req) · Data cache (cross-request) · Full route cache (build-time) · Router cache (client). revalidateTag / revalidatePath để invalidate.

Pitfall: Pass non-serializable (function, Date raw) từ Server → Client → error. "use client" bừa → mất benefit RSC. Quên revalidate sau mutation → user thấy stale.

4 · Performance Optimization

Mantra: Đo trước, optimize sau. React Profiler + Lighthouse + Chrome Performance.

Code splitting

dynamic(() => import('./Heavy')) · route-based + component-based.

Image optimization

Next.js <Image> — auto WebP/AVIF, lazy load, responsive srcset.

Virtual list

10k items → TanStack Virtual / react-window. Chỉ render visible.

Bundle analysis

@next/bundle-analyzer — thấy package nào nặng nhất.

Re-render reduce

State local · split component · React.memo + stable props · Context split.

Core Web Vitals

LCP < 2.5s · INP < 200ms · CLS < 0.1. Track real-user qua Vercel Analytics.

Pitfall: Premature optimization (memoize mọi component). Bundle import full lodash thay vì lodash/get. CSS-in-JS runtime nặng — pick Tailwind / CSS Modules cho perf.

← Backend Next: Principles →
🔥 FE DEEP

Phase 3 — Frontend Deep Dive

React Core · Controlled vs Uncontrolled · Complex Forms · AbortController · FE Async · TanStack Query Deep

⚛️ React Core

1. Hooks Rules — 2 luật bất biến

  1. Only at top level — KHÔNG dùng trong if/for/while/nested function
  2. Only in components or custom hooks — KHÔNG dùng trong utility function
// ❌ VIOLATIONS
function MyComponent({ flag }) {
  if (flag) useState(0);          // conditional → React detect được + crash
  for (let i = 0; i < 5; i++) useState(i);  // loop → order không stable
  const click = () => useState(0);  // inside event handler → wrong
}

// ✅ CORRECT
function MyComponent({ flag }) {
  const [count, setCount] = useState(0);  // top level
  if (flag) { /* use count here OK */ }
}

Why: React track hooks bằng call order. Re-render với order khác → state mapping sai → crash. Linter react-hooks/rules-of-hooks enforce.

2. useMemo / useCallback — KHI NÀO worth it

Default position: KHÔNG dùng. Cache cũng tốn cycle CPU (check deps + lookup). Chỉ dùng khi có measurable benefit.

3 trường hợp justified:

  1. Phép tính thật sự nặng — sort 10k items, filter phức tạp (>10ms)
  2. Reference identity matter — pass object/function xuống child wrap React.memo()
  3. Tránh useEffect re-run vô tận — dep là object/function tạo mới mỗi render
// ✅ Justified — expensive sort
const sortedItems = useMemo(
  () => items.sort((a, b) => expensive(a, b)),
  [items]
);

// ✅ Justified — passed to memoized child
const handleClick = useCallback(() => doSomething(id), [id]);
return <ExpensiveChild onClick={handleClick} />;  // child wrapped in React.memo

// ❌ OVERUSE — không có lợi
const doubled = useMemo(() => x * 2, [x]);

React 19: React Compiler auto-memoize. Đa số dev không cần manual nữa. Nhưng vẫn phải hiểu nguyên lý để debug khi compiler skip.

3. React 18+ Features

useTransition — mark update là non-urgent:

const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
  setQuery(e.target.value);  // urgent — UI input
  startTransition(() => setFilteredResults(filterHeavy(items, e.target.value)));
};

Suspense + use() (React 19):

function UserList() {
  const users = use(fetchUsers());  // Suspends if Promise unresolved
  return <List users={users} />;
}
<Suspense fallback={<Spinner />}><UserList /></Suspense>

RSC (Server Components):

Server ComponentsClient Components
Default trong App RouterNeed 'use client'
async + fetch direct + DB accessuseState/useEffect needed
Zero JS to clientHydrate on client
No hooks, no browser APIsFull React power

Rule: Push 'use client' xuống tận leaf để minimize JS payload.

🎛️ Controlled vs Uncontrolled Inputs

Phân biệt: AI giữ giá trị input?

LoạiSource of truth
ControlledReact state giữ. DOM chỉ render lại từ state.
UncontrolledDOM giữ. React đọc qua ref khi cần.
// Controlled
function Form() {
  const [name, setName] = useState('');
  return <input value={name} onChange={(e) => setName(e.target.value)} />;
}

// Uncontrolled
function Form() {
  const ref = useRef<HTMLInputElement>(null);
  const handleSubmit = () => console.log(ref.current?.value);
  return <input ref={ref} defaultValue="" />;
}

Trade-off

Tiêu chíControlledUncontrolled
PerformanceRe-render mỗi keystrokeKhông re-render
Validate realtimeDễKhó (qua ref)
Format/MaskDễKhó
Reset programmaticallysetName('')Imperative ref
File input❌ Cấm✅ Bắt buộc
React-wayIdiomaticEscape hatch

Pitfalls quan trọng

// ❌ Quên onChange trong controlled — input "đóng băng"
<input value={name} />
// Warning: "value without onChange = read-only field"

// ❌ Mix value và defaultValue
<input value={name} defaultValue="John" onChange={setName} />
// Warning: "uncontrolled → controlled switch"

// ❌ Initial state = undefined → switch warning
const [name, setName] = useState();  // ← undefined
<input value={name} onChange={(e) => setName(e.target.value)} />

// ✅ Init với value cùng kiểu
const [name, setName] = useState('');  // empty string KHÔNG undefined

📋 Complex Forms — CRM/ERP Style

1. Tại sao Form 50-60 fields KHÁC BIỆT

2. Stack chuẩn 2025: Zod + RHF + UI lib

import { z } from 'zod';

const employeeSchema = z.object({
  fullName: z.string().min(2).max(100),
  email: z.string().email(),
  phone: z.string().regex(/^(0|\+84)\d{9}$/),
  salary: z.number().int().positive(),
  address: z.object({ street: z.string(), district: z.string(), city: z.string() }),
  dependents: z.array(z.object({
    name: z.string(),
    relationship: z.enum(['spouse', 'child', 'parent']),
  })),
  contractType: z.discriminatedUnion('type', [
    z.object({ type: z.literal('full-time'), monthlySalary: z.number() }),
    z.object({ type: z.literal('contractor'), hourlyRate: z.number() }),
  ]),
});

type Employee = z.infer<typeof employeeSchema>;

Magic: 1 schema → TS types + runtime validation. FE-BE đồng bộ. Composable.

3. RHF + Zod — Uncontrolled bên trong, validate như controlled

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

function EmployeeForm() {
  const form = useForm<Employee>({
    resolver: zodResolver(employeeSchema),
    mode: 'onTouched',  // ← validate sau khi rời field lần đầu
  });

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <input {...form.register('fullName')} />
      {form.formState.errors.fullName && (
        <span>{form.formState.errors.fullName.message}</span>
      )}
    </form>
  );
}

Key: {...register} trả về ref+onChange DOM thuần → KHÔNG trigger parent re-render. Form 100 fields, gõ field 50 → CHỈ field 50 re-render.

4. Validation mode — chọn timing

ModeValidate khiUX
'onSubmit' (default)User submitTệ — user phải submit mới biết sai
'onBlur'Rời fieldDecent
'onChange'Mỗi keystrokeRealtime, best cho password
'onTouched'First blur, sau đó onChangeBest default cho signup
'all'onBlur + onChangeComprehensive

5. Password Realtime Validation + Checklist Pattern

const passwordSchema = z.string()
  .min(8, 'Tối thiểu 8 ký tự')
  .refine(s => /[A-Z]/.test(s), 'Cần chữ HOA')
  .refine(s => /[a-z]/.test(s), 'Cần chữ thường')
  .refine(s => /\d/.test(s), 'Cần chữ số')
  .refine(s => /[^A-Za-z0-9]/.test(s), 'Cần ký tự đặc biệt');

const schema = z.object({
  password: passwordSchema,
  confirmPassword: z.string(),
}).refine(d => d.password === d.confirmPassword, {
  message: 'Mật khẩu xác nhận không khớp',
  path: ['confirmPassword'],  // ← critical: gắn error vào confirmPassword
});

UX nâng cấp — Checklist live update:

// Sub-component để cô lập re-render
function PasswordChecklist({ control }) {
  const pw = useWatch({ control, name: 'password', defaultValue: '' });
  const checks = [
    { label: '≥ 8 ký tự', pass: pw.length >= 8 },
    { label: 'Có chữ HOA', pass: /[A-Z]/.test(pw) },
    { label: 'Có chữ thường', pass: /[a-z]/.test(pw) },
    { label: 'Có chữ số', pass: /\d/.test(pw) },
    { label: 'Có ký tự đặc biệt', pass: /[^A-Za-z0-9]/.test(pw) },
  ];
  return <ul>{checks.map(c => <li className={c.pass ? 'text-green-500' : 'text-gray-500'}>{c.pass ? '✓' : '○'} {c.label}</li>)}</ul>;
}

Tại sao useWatch trong sub-component: chỉ component này re-render mỗi keystroke. Parent form (50 fields khác) im lặng.

6. RHF vs TanStack Form (2026 comparison)

Tiêu chíRHFTanStack Form
Tuổi/Maturity2019, mature2024, GA mới
FrameworkReact onlyReact, Vue, Solid, Svelte
TypeScriptTốtTốt hơn (no generic juggling)
ValidationZod via resolverZod first-class, no resolver
Async validationCustomFirst-class + debounced
Cross-fieldwatch + setValue tayField listeners declarative
EcosystemKhủng (shadcn, MUI)Đang lớn nhanh

Verdict: RHF default an toàn. TanStack Form pick cho greenfield TS-heavy, cross-field deps phức tạp, hoặc unified với TanStack Query.

7. Field Types "Dị Giáo"

FieldLibrary 2025
File uploadNative + presigned URL → S3/R2 (bypass backend)
Image với cropreact-easy-crop
Rich textTipTap (ProseMirror) / Lexical (Meta)
Date rangereact-day-picker / MUI X DateRangePicker
Multi-select / Comboboxreact-select / cmdk (shadcn)
Tags inputreact-tag-input / custom với onKeyDown
Address autocompleteGoong Maps (VN) / Google Places
Phone + country codereact-phone-number-input + libphonenumber-js
Signaturereact-signature-canvas
OTP / PINshadcn InputOTP / react-otp-input
Color pickerreact-colorful
Slider / rangeRadix Slider / MUI Slider

8. Perf cho 50-60 fields

🛑 AbortController Deep

Core concept

AbortController = signal generator. 1 controller signal N consumers. Abort → all stop.

const controller = new AbortController();
const { signal } = controller;

// Multiple consumers
fetch(url1, { signal });
fetch(url2, { signal });
element.addEventListener('click', handler, { signal });

controller.abort();  // 1 lệnh kill TẤT CẢ

❌ Race condition KHÔNG có AbortController:

sequenceDiagram autonumber participant U as User participant C as Component participant API as API U->>C: search "ap" C->>API: fetch /search?q=ap (slow, 800ms) U->>C: search "apple" (typed nhanh) C->>API: fetch /search?q=apple (fast, 100ms) API-->>C: 200 results for "apple" ✅ C->>C: setState(applesResults) Note over C: User thấy kết quả "apple" API-->>C: 200 results for "ap" (chậm về sau) C->>C: setState(apResults) 💀 Note over U: 💀 User thấy kết quả "ap" dù đã type "apple"

✅ Fix với AbortController trong useEffect cleanup:

sequenceDiagram autonumber participant U as User participant C as Component participant Ctrl1 as Controller 1 participant Ctrl2 as Controller 2 participant API as API U->>C: search "ap" C->>Ctrl1: new AbortController() C->>API: fetch /search?q=ap signal=ctrl1 U->>C: search "apple" C->>Ctrl1: ctrl1.abort() ← cleanup useEffect Note over API: request 1 cancelled ❌ C->>Ctrl2: new AbortController() C->>API: fetch /search?q=apple signal=ctrl2 API-->>C: 200 results "apple" ✅ C->>C: setState(applesResults) Note over U: ✅ User thấy đúng kết quả "apple"

5 Use Cases

1. fetch trong useEffect:

useEffect(() => {
  const ctrl = new AbortController();
  fetch(url, { signal: ctrl.signal })
    .then(r => r.json()).then(setData)
    .catch(err => {
      if (err.name === 'AbortError') return;  // expected — silent
      console.error(err);
    });
  return () => ctrl.abort();
}, [url]);

2. addEventListener với signal (modern):

useEffect(() => {
  const ctrl = new AbortController();
  window.addEventListener('resize', h1, { signal: ctrl.signal });
  document.addEventListener('keydown', h2, { signal: ctrl.signal });
  return () => ctrl.abort();  // 1 lệnh cleanup TẤT CẢ
}, []);

3. Timeout pattern (ES2022):

await fetch(url, { signal: AbortSignal.timeout(5000) });

// Hoặc combine với user cancel:
const signal = AbortSignal.any([userController.signal, AbortSignal.timeout(5000)]);
await fetch(url, { signal });

4. Manual user-triggered cancel:

const controllerRef = useRef<AbortController | null>(null);
const startUpload = () => {
  controllerRef.current = new AbortController();
  fetch('/upload', { signal: controllerRef.current.signal, ... });
};
const cancel = () => controllerRef.current?.abort();

5. Cancel multiple concurrent fetches:

const ctrl = new AbortController();
await Promise.all([
  fetch(url1, { signal: ctrl.signal }),
  fetch(url2, { signal: ctrl.signal }),
  fetch(url3, { signal: ctrl.signal }),
]);
// ctrl.abort() cancel cả 3

Anatomy + Pitfalls

const ctrl = new AbortController();
ctrl.signal           // AbortSignal — read-only handle
ctrl.abort(reason?)   // trigger abort
ctrl.signal.aborted   // boolean
ctrl.signal.reason    // lý do (string/Error)

// ❌ Pitfalls
ctrl.abort();
fetch(url, { signal: ctrl.signal });  // immediately rejects — reuse fail

.catch(err => showToast(err.message));  // "AbortError" toast → confused user

useEffect(() => { ... }, []);  // empty deps + abort cleanup → chỉ cancel khi unmount
// Nếu muốn cancel khi `url` đổi → deps phải có url

One-shot: AbortController không reuse được. Mỗi request cycle → tạo controller mới.

🎨 FE Async — Race & Patterns

1. useEffect Race — Stale response problem

Bug: Fetch /search?q=a trả về sau /search?q=ab → UI sai.

Fix: AbortController trong cleanup (xem section trên).

2. Stale Closure — setInterval không update?

// ❌ BUG — count "đóng băng" ở 0
useEffect(() => {
  const timer = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(timer);
}, []);  // empty deps + closure capture count = 0

// ✅ Functional updater
useEffect(() => {
  const timer = setInterval(() => setCount(prev => prev + 1), 1000);
  return () => clearInterval(timer);
}, []);

Why functional safer: Closure capture giá trị tại thời điểm define. Functional updater bypass closure — React give latest state from queue.

3. Debouncing vs Throttling

PatternFiresUse case
DebouncingSau khi ngừng X msSearch, autosave
ThrottlingMỗi X ms đều đặnScroll, mousemove, infinite scroll
function useDebouncedValue<T>(value: T, ms: number): T {
  const [d, setD] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setD(value), ms);
    return () => clearTimeout(id);
  }, [value, ms]);
  return d;
}
// Sweet spot: 300-500ms cho search

🔄 TanStack Query Deep

1. Server state ≠ Client state

Server state có 7 đặc tính client state KHÔNG có: source on remote, can stale, async, multiple subscribers, loading/error states, refetch triggers, race conditions. TanStack Query là state manager dành riêng cho server state.

2. Core API

const { data, isPending, isFetching, error } = useQuery({
  queryKey: ['user', userId],                              // ← unique cache key
  queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
  staleTime: 60_000,                                        // ← fresh trong 60s
});

3 thứ quan trọng nhất: queryKey (identity) · queryFn (how to fetch) · staleTime (when to refetch).

3. isPending vs isFetching (HAY HỎI)

StateMeaningWhen show
isPendingChưa có data cache nào (first load)Full skeleton
isFetchingĐang fetch (kể cả background)Subtle indicator (top bar)
isErrorFailedError UI
isSuccessSucceededRender data

Stale-While-Revalidate UX: Show stale data + subtle loader thay vì blank screen mỗi refetch.

4. staleTime vs gcTime

staleTime: thời gian data được coi FRESH (không tự refetch)

gcTime: thời gian giữ data trong cache sau khi KHÔNG component nào subscribe

Data typestaleTimegcTime
User profile (rarely changes)5 phút30 phút
Dashboard metrics30 giây5 phút
Real-time chat05 phút
Config/feature flags30 phút1 giờ

5. Optimistic Update — 4 steps memorize

sequenceDiagram autonumber actor U as User participant UI as UI participant QC as QueryClient
(cache) participant API as Server U->>UI: click "Like" Note over UI,QC: onMutate begins UI->>QC: ① CANCEL queries đang fetch UI->>QC: ② SNAPSHOT previous (rollback context) UI->>QC: ③ SET optimistic data (liked=true) Note over U: User thấy UI update NGAY ⚡ UI->>API: POST /tasks/:id/like alt ✅ Success API-->>UI: 200 UI->>QC: ④b INVALIDATE → refetch fresh data else ❌ Error API-->>UI: 500 UI->>QC: ④a ROLLBACK (restore snapshot) Note over U: User thấy UI revert + toast error end
useMutation({
  mutationFn: (id) => api.likeTask(id),
  onMutate: async (id) => {
    // 1. CANCEL queries đang chạy
    await queryClient.cancelQueries({ queryKey: ['task', id] });
    // 2. SNAPSHOT previous để rollback
    const previous = queryClient.getQueryData(['task', id]);
    // 3. SET optimistic data
    queryClient.setQueryData(['task', id], (old) => ({ ...old, liked: true }));
    return { previous };
  },
  onError: (_, id, ctx) => {
    queryClient.setQueryData(['task', id], ctx.previous);  // 4a. ROLLBACK
  },
  onSettled: (_, __, id) => {
    queryClient.invalidateQueries({ queryKey: ['task', id] });  // 4b. INVALIDATE
  },
});

6. Query Key Factory Pattern (senior signal)

// queries/task-keys.ts
export const taskKeys = {
  all: ['tasks'] as const,
  lists: () => [...taskKeys.all, 'list'] as const,
  list: (filters: TaskFilters) => [...taskKeys.lists(), filters] as const,
  details: () => [...taskKeys.all, 'detail'] as const,
  detail: (id: string) => [...taskKeys.details(), id] as const,
};

useQuery({ queryKey: taskKeys.detail(id), ... });

// Bulk invalidation theo hierarchy
queryClient.invalidateQueries({ queryKey: taskKeys.all });    // ALL task queries
queryClient.invalidateQueries({ queryKey: taskKeys.lists() }); // Only lists

Lợi ích: Type-safe, refactor-friendly, no magic strings, hierarchy invalidation dễ.

7. `meta` Option — Metadata sidecar

meta = arbitrary metadata gắn vào query, KHÔNG ảnh hưởng cache identity. Access tại queryFn context + QueryCache global handlers.

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  meta: { errorMessage: 'Không tải được nhân viên' },
});

// Global handler:
new QueryCache({
  onError: (error, query) => {
    toast.error((query.meta?.errorMessage as string) ?? 'Lỗi');
  },
});

5 production patterns:

  1. Custom error messages per query
  2. Conditional invalidation by tag (predicate: q => q.meta?.tag === true)
  3. Analytics tracking (meta.trackPerformance)
  4. Authorization metadata (meta.requiresRole)
  5. UI hints (meta.showCacheBadge)

Type-safe: module augmentation Register.queryMeta → query.meta auto-typed.

8. Refetch triggers

useQuery({
  queryKey: ['user', id],
  queryFn: fetchUser,
  refetchOnMount: true,           // default
  refetchOnWindowFocus: true,     // default — great cho dashboards
  refetchOnReconnect: true,       // default
  refetchInterval: 5_000,         // polling — dùng cẩn thận
  enabled: !!id,                  // conditional execution
});

9. Infinite Queries — pagination

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
  queryKey: ['tasks', 'infinite'],
  queryFn: ({ pageParam = 0 }) =>
    fetch(`/api/tasks?cursor=${pageParam}`).then(r => r.json()),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
});
// Cursor > offset: stable, perf tốt hơn cho large datasets

10. Common Pitfalls