🎯 REACT SENIOR COMPLETE WORKFLOW
📋 MỤC ĐÍCH
Chuẩn hóa workflow làm bài tập React ở level Senior/Lead, bao phủm TOÀN BỘ kiến thức ReactJS, đảm bảo code production-ready và phù hợp cho technical interview.
✅ CHECKLIST CHUẨN - ÁP DỤNG CHO MỌI BÀI
🧠 A. Cách Làm Bài
- [ ] User chỉ trình bày ý tưởng / hướng giải
- [ ] AI review & tối ưu theo Senior mindset
- [ ] AI viết full code hoàn chỉnh để copy
- [ ] Code phải chạy được ngay, không placeholder
🏗️ I. STATE MANAGEMENT
📌 1. useState Mastery
Basic Rules
- [ ] Không lạm dụng useState cho mọi thứ
- [ ] Nhóm related states thành object
- [ ] KHÔNG lưu derived state vào state
typescript
// ❌ BAD: Derived state
const [count, setCount] = useState(0);
const [doubled, setDoubled] = useState(0); // Derived!
// ✅ GOOD: Compute on render
const [count, setCount] = useState(0);
const doubled = count * 2;State Grouping
typescript
// ❌ BAD: Nhiều useState rời rạc
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
// ✅ GOOD: Nhóm related data
const [user, setUser] = useState({
firstName: '',
lastName: '',
email: '',
});State Updates
typescript
// ❌ BAD: Direct mutation
state.items.push(newItem);
setState(state);
// ✅ GOOD: Immutable update
setState((prev) => ({
...prev,
items: [...prev.items, newItem],
}));Functional Updates
typescript
// ❌ BAD: Closure stale state
setCount(count + 1);
// ✅ GOOD: Function form khi cần previous state
setCount((prev) => prev + 1);📌 2. useReducer
Khi Nào Dùng useReducer
- State logic phức tạp (nhiều sub-values)
- State transitions có rules rõ ràng
- Cần test logic tách biệt khỏi component
- Multiple related state updates
typescript
// State phức tạp với nhiều actions
type State = {
past: number[];
present: number;
future: number[];
limits: { min: number; max: number };
};
type Action =
| { type: 'INCREMENT'; step: number }
| { type: 'DECREMENT'; step: number }
| { type: 'UNDO' }
| { type: 'REDO' }
| { type: 'RESET' }
| { type: 'SET_LIMITS'; limits: { min: number; max: number } };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT':
const newValue = clamp(
state.present + action.step,
state.limits.min,
state.limits.max
);
return {
...state,
past: [...state.past, state.present],
present: newValue,
future: [],
};
case 'UNDO':
if (state.past.length === 0) return state;
return {
...state,
past: state.past.slice(0, -1),
present: state.past[state.past.length - 1],
future: [state.present, ...state.future],
};
// ... other cases
default:
return state;
}
}
// Usage
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'INCREMENT', step: 5 });Testing Reducer
typescript
describe('counterReducer', () => {
test('should increment value', () => {
const state = { present: 5, past: [0], future: [] };
const action = { type: 'INCREMENT', step: 3 };
const newState = reducer(state, action);
expect(newState.present).toBe(8);
expect(newState.past).toEqual([0, 5]);
});
});📌 3. useContext
Context Structure
typescript
// contexts/ThemeContext.tsx
type Theme = 'light' | 'dark';
type ThemeContextType = {
theme: Theme;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = useCallback(() => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
}, []);
// PERFORMANCE: Memoize value để tránh unnecessary re-renders
const value = useMemo(
() => ({
theme,
toggleTheme,
}),
[theme, toggleTheme]
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
// Custom hook
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}Multiple Contexts Pattern
typescript
// Compose multiple providers
export function AppProviders({ children }: { children: ReactNode }) {
return (
<ThemeProvider>
<AuthProvider>
<DataProvider>{children}</DataProvider>
</AuthProvider>
</ThemeProvider>
);
}📌 4. Custom Hooks
Extract Reusable Logic
typescript
// hooks/useLocalStorage.ts
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('Error reading localStorage:', error);
return initialValue;
}
});
const setValue = useCallback(
(value: T | ((prev: T) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('Error writing to localStorage:', error);
}
},
[key, storedValue]
);
return [storedValue, setValue] as const;
}
// Usage
const [user, setUser] = useLocalStorage('user', { name: '' });Async Data Fetching Hook
typescript
function useAsync<T>(asyncFn: () => Promise<T>, dependencies: any[] = []) {
const [state, setState] = useState<{
data: T | null;
loading: boolean;
error: Error | null;
}>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
let isMounted = true;
setState({ data: null, loading: true, error: null });
asyncFn()
.then((data) => {
if (isMounted) {
setState({ data, loading: false, error: null });
}
})
.catch((error) => {
if (isMounted) {
setState({ data: null, loading: false, error });
}
});
// EDGE CASE: Cleanup để tránh memory leak
return () => {
isMounted = false;
};
}, dependencies);
return state;
}
// Usage
const { data, loading, error } = useAsync(
() => fetch('/api/users').then((r) => r.json()),
[]
);Form Handling Hook
typescript
function useForm<T extends Record<string, any>>(initialValues: T) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>(
{}
);
const handleChange = useCallback((name: keyof T, value: any) => {
setValues((prev) => ({ ...prev, [name]: value }));
}, []);
const handleBlur = useCallback((name: keyof T) => {
setTouched((prev) => ({ ...prev, [name]: true }));
}, []);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
}, [initialValues]);
return {
values,
errors,
touched,
handleChange,
handleBlur,
reset,
setErrors,
};
}⚡ II. PERFORMANCE OPTIMIZATION
📌 1. useMemo
Khi Nào Dùng
- Expensive calculations
- Reference equality matters (dependencies)
- Preventing unnecessary child re-renders
typescript
function ExpensiveComponent({ items, filter }: Props) {
// PERFORMANCE: Memoize expensive filtering
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter((item) => item.category === filter);
}, [items, filter]);
// ❌ BAD: Compute on every render
// const filteredItems = items.filter(item => item.category === filter);
return <List items={filteredItems} />;
}Memoize Objects/Arrays
typescript
function Parent() {
const [count, setCount] = useState(0);
// ❌ BAD: New object every render
const config = { theme: 'dark', size: 'large' };
// ✅ GOOD: Memoized object
const config = useMemo(
() => ({
theme: 'dark',
size: 'large',
}),
[]
);
return <Child config={config} />;
}📌 2. useCallback
Memoize Functions
typescript
function Parent() {
const [count, setCount] = useState(0);
// ❌ BAD: New function every render
const handleClick = () => {
console.log('Clicked');
};
// ✅ GOOD: Memoized function
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
// With dependencies
const handleIncrement = useCallback(() => {
setCount((prev) => prev + 1);
}, []); // No dependencies vì dùng functional update
return <Child onClick={handleClick} />;
}📌 3. React.memo
Prevent Unnecessary Re-renders
typescript
// Child component
type ChildProps = {
name: string;
onClick: () => void;
};
const Child = React.memo(({ name, onClick }: ChildProps) => {
console.log('Child rendered');
return <button onClick={onClick}>{name}</button>;
});
// Custom comparison
const Child = React.memo(
({ name, onClick }: ChildProps) => {
return <button onClick={onClick}>{name}</button>;
},
(prevProps, nextProps) => {
// Return true nếu props không thay đổi (skip re-render)
return prevProps.name === nextProps.name;
}
);📌 4. Code Splitting & Lazy Loading
typescript
// Lazy load components
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path='/dashboard' element={<Dashboard />} />
<Route path='/settings' element={<Settings />} />
</Routes>
</Suspense>
);
}🔄 III. SIDE EFFECTS
📌 1. useEffect
Basic Rules
typescript
// ❌ BAD: Missing dependencies
useEffect(() => {
console.log(count);
}, []); // count should be in deps
// ✅ GOOD: Correct dependencies
useEffect(() => {
console.log(count);
}, [count]);
// ❌ BAD: Object in dependencies (recreated every render)
useEffect(() => {
fetchData(config);
}, [config]); // config is object
// ✅ GOOD: Destructure or memoize
useEffect(() => {
fetchData({ theme, size });
}, [theme, size]);Cleanup Functions
typescript
// Subscription cleanup
useEffect(() => {
const subscription = dataSource.subscribe(handleData);
// EDGE CASE: Cleanup để tránh memory leak
return () => {
subscription.unsubscribe();
};
}, []);
// Event listener cleanup
useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
// Abort fetch cleanup
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then((r) => r.json())
.then(setData)
.catch((err) => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
return () => {
controller.abort();
};
}, []);Effect Patterns
typescript
// Mount only (componentDidMount)
useEffect(() => {
console.log('Mounted');
}, []);
// Update only (componentDidUpdate)
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
console.log('Updated');
}, [dependency]);
// Unmount only (componentWillUnmount)
useEffect(() => {
return () => {
console.log('Unmounting');
};
}, []);📌 2. useLayoutEffect
Khi Nào Dùng
- Đo DOM trước khi paint
- Sync DOM mutations
- Avoid visual flicker
typescript
// Measure DOM element
function Tooltip() {
const ref = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
useLayoutEffect(() => {
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
setPosition({ x: rect.x, y: rect.y });
}
}, []);
return <div ref={ref} style={{ left: position.x, top: position.y }} />;
}🎣 IV. REF PATTERNS
📌 1. useRef
DOM References
typescript
function Input() {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>Focus</button>
</>
);
}Mutable Values (không trigger re-render)
typescript
function Timer() {
const intervalRef = useRef<number | null>(null);
const [count, setCount] = useState(0);
const startTimer = () => {
intervalRef.current = window.setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
};
const stopTimer = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
useEffect(() => {
return () => stopTimer();
}, []);
return <div>{count}</div>;
}Previous Value Pattern
typescript
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Usage
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);📌 2. useImperativeHandle
typescript
// Expose custom ref methods
type InputRef = {
focus: () => void;
clear: () => void;
};
const CustomInput = forwardRef<InputRef, InputProps>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
}
},
}));
return <input ref={inputRef} {...props} />;
});
// Usage
const ref = useRef<InputRef>(null);
<CustomInput ref={ref} />;
ref.current?.focus();🧩 V. COMPONENT PATTERNS
📌 1. Compound Components
typescript
// Tabs.tsx
type TabsContextType = {
activeTab: string;
setActiveTab: (tab: string) => void;
};
const TabsContext = createContext<TabsContextType | undefined>(undefined);
export function Tabs({ children, defaultTab }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className='tabs'>{children}</div>
</TabsContext.Provider>
);
}
Tabs.List = function TabsList({ children }: { children: ReactNode }) {
return <div className='tabs__list'>{children}</div>;
};
Tabs.Tab = function Tab({ id, children }: TabProps) {
const { activeTab, setActiveTab } = useContext(TabsContext)!;
return (
<button
className={`tabs__tab ${
activeTab === id ? 'tabs__tab--active' : ''
}`}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
};
Tabs.Panel = function TabPanel({ id, children }: TabPanelProps) {
const { activeTab } = useContext(TabsContext)!;
if (activeTab !== id) return null;
return <div className='tabs__panel'>{children}</div>;
};
// Usage
<Tabs defaultTab='tab1'>
<Tabs.List>
<Tabs.Tab id='tab1'>Tab 1</Tabs.Tab>
<Tabs.Tab id='tab2'>Tab 2</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id='tab1'>Content 1</Tabs.Panel>
<Tabs.Panel id='tab2'>Content 2</Tabs.Panel>
</Tabs>;📌 2. Render Props
typescript
type MouseTrackerProps = {
render: (position: { x: number; y: number }) => ReactNode;
};
function MouseTracker({ render }: MouseTrackerProps) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return <>{render(position)}</>;
}
// Usage
<MouseTracker
render={({ x, y }) => (
<div>
Mouse at: {x}, {y}
</div>
)}
/>;📌 3. Higher-Order Components (HOC)
typescript
// withAuth HOC
function withAuth<P extends object>(Component: ComponentType<P>) {
return function AuthenticatedComponent(props: P) {
const { user, loading } = useAuth();
if (loading) return <Spinner />;
if (!user) return <Navigate to='/login' />;
return <Component {...props} />;
};
}
// Usage
const ProtectedDashboard = withAuth(Dashboard);📌 4. Container/Presenter Pattern
typescript
// UserListContainer.tsx (Logic)
function UserListContainer() {
const { data, loading, error } = useAsync(fetchUsers);
const [filter, setFilter] = useState('');
const filteredUsers = useMemo(() => {
if (!data) return [];
return data.filter((user) =>
user.name.toLowerCase().includes(filter.toLowerCase())
);
}, [data, filter]);
return (
<UserListPresenter
users={filteredUsers}
loading={loading}
error={error}
filter={filter}
onFilterChange={setFilter}
/>
);
}
// UserListPresenter.tsx (UI)
type PresenterProps = {
users: User[];
loading: boolean;
error: Error | null;
filter: string;
onFilterChange: (value: string) => void;
};
function UserListPresenter({
users,
loading,
error,
filter,
onFilterChange,
}: PresenterProps) {
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return (
<div className='user-list'>
<input
value={filter}
onChange={(e) => onFilterChange(e.target.value)}
placeholder='Search users...'
/>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}🎨 VI. STYLING APPROACHES
📌 1. BEM Convention
typescript
function Button({ variant, size, disabled }: ButtonProps) {
const className = [
'button',
`button--${variant}`,
`button--${size}`,
disabled && 'button--disabled',
]
.filter(Boolean)
.join(' ');
return <button className={className}>Click me</button>;
}📌 2. CSS Modules
typescript
import styles from './Button.module.css';
function Button() {
return <button className={styles.button}>Click me</button>;
}📌 3. Styled Components (Optional)
typescript
import styled from 'styled-components';
const StyledButton = styled.button<{ variant: string }>`
padding: 8px 16px;
background: ${(props) => (props.variant === 'primary' ? 'blue' : 'gray')};
&:hover {
opacity: 0.8;
}
`;📌 4. Tailwind CSS
typescript
function Button({ variant }: ButtonProps) {
const baseClasses = 'px-4 py-2 rounded font-semibold';
const variantClasses =
variant === 'primary'
? 'bg-blue-500 text-white hover:bg-blue-600'
: 'bg-gray-200 text-gray-800 hover:bg-gray-300';
return (
<button className={`${baseClasses} ${variantClasses}`}>Click me</button>
);
}🧪 VII. TESTING
📌 1. Component Testing
typescript
describe('Counter', () => {
test('should increment on button click', () => {
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
const count = screen.getByText('0');
fireEvent.click(button);
expect(screen.getByText('1')).toBeInTheDocument();
});
test('should handle multiple increments', () => {
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
fireEvent.click(button);
fireEvent.click(button);
fireEvent.click(button);
expect(screen.getByText('3')).toBeInTheDocument();
});
});📌 2. Hook Testing
typescript
import { renderHook, act } from '@testing-library/react';
describe('useCounter', () => {
test('should increment', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});📌 3. Async Testing
typescript
test('should load and display data', async () => {
render(<UserList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
const users = await screen.findByText('John Doe');
expect(users).toBeInTheDocument();
});📌 4. Mock Testing
typescript
// Mock API
jest.mock('../api/users', () => ({
fetchUsers: jest.fn(() => Promise.resolve([{ id: 1, name: 'John' }])),
}));
test('should fetch users on mount', async () => {
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('John')).toBeInTheDocument();
});
expect(fetchUsers).toHaveBeenCalledTimes(1);
});📝 VIII. TYPESCRIPT PATTERNS
📌 1. Component Props
typescript
// Basic props
type ButtonProps = {
children: ReactNode;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
};
// Extending HTML props
type InputProps = {
label: string;
error?: string;
} & React.InputHTMLAttributes<HTMLInputElement>;
// Generic component
type ListProps<T> = {
items: T[];
renderItem: (item: T) => ReactNode;
keyExtractor: (item: T) => string;
};
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}📌 2. Event Types
typescript
const handleClick = (e: MouseEvent<HTMLButtonElement>): void => {};
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {};
const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {};📌 3. Ref Types
typescript
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
const timeoutRef = useRef<number | null>(null);🎯 IX. ADVANCED PATTERNS
📌 1. Error Boundaries
typescript
type ErrorBoundaryState = {
hasError: boolean;
error: Error | null;
};
class ErrorBoundary extends Component<
{ children: ReactNode },
ErrorBoundaryState
> {
state: ErrorBoundaryState = {
hasError: false,
error: null,
};
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <ErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<App />
</ErrorBoundary>;📌 2. Portal
typescript
function Modal({ children, isOpen }: ModalProps) {
if (!isOpen) return null;
return createPortal(
<div className='modal'>
<div className='modal__backdrop' />
<div className='modal__content'>{children}</div>
</div>,
document.getElementById('modal-root')!
);
}📌 3. Suspense & Concurrent Features
typescript
// Data fetching with Suspense
const resource = fetchData();
function UserProfile() {
const user = resource.read(); // Suspend khi loading
return <div>{user.name}</div>;
}
// Usage
<Suspense fallback={<Spinner />}>
<UserProfile />
</Suspense>;
// useTransition
function SearchResults() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleSearch = (value: string) => {
startTransition(() => {
setQuery(value); // Low priority update
});
};
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} />
{isPending && <Spinner />}
<Results query={query} />
</>
);
}🚀 X. ROUTING (React Router)
📌 1. Basic Setup
typescript
import { BrowserRouter, Routes, Route, Link, Navigate } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
<Link to='/users'>Users</Link>
</nav>
<Routes>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
<Route path='/users' element={<Users />} />
<Route path='/users/:id' element={<UserDetail />} />
<Route path='*' element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}📌 2. Nested Routes
typescript
function App() {
return (
<Routes>
<Route path='/' element={<Layout />}>
<Route index element={<Home />} />
<Route path='about' element={<About />} />
<Route path='dashboard' element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path='analytics' element={<Analytics />} />
<Route path='settings' element={<Settings />} />
</Route>
</Route>
</Routes>
);
}
// Layout component
function Layout() {
return (
<div>
<header>Header</header>
<main>
<Outlet /> {/* Nested routes render here */}
</main>
<footer>Footer</footer>
</div>
);
}📌 3. Dynamic Routes & Params
typescript
import { useParams, useSearchParams } from 'react-router-dom';
function UserDetail() {
const { id } = useParams<{ id: string }>();
const [searchParams, setSearchParams] = useSearchParams();
const tab = searchParams.get('tab') || 'profile';
return (
<div>
<h1>User {id}</h1>
<nav>
<button onClick={() => setSearchParams({ tab: 'profile' })}>
Profile
</button>
<button onClick={() => setSearchParams({ tab: 'posts' })}>
Posts
</button>
</nav>
{tab === 'profile' && <UserProfile userId={id!} />}
{tab === 'posts' && <UserPosts userId={id!} />}
</div>
);
}📌 4. Protected Routes
typescript
type ProtectedRouteProps = {
children: ReactElement;
};
function ProtectedRoute({ children }: ProtectedRouteProps) {
const { user, loading } = useAuth();
if (loading) {
return <LoadingSpinner />;
}
if (!user) {
return <Navigate to='/login' replace />;
}
return children;
}
// Usage
<Routes>
<Route path='/login' element={<Login />} />
<Route
path='/dashboard'
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
</Routes>;📌 5. Navigation Hooks
typescript
import { useNavigate, useLocation } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname || '/dashboard';
const handleLogin = async (credentials: Credentials) => {
await login(credentials);
navigate(from, { replace: true });
};
return <form onSubmit={handleLogin}>...</form>;
}
// Programmatic navigation
function ProductList() {
const navigate = useNavigate();
const handleProductClick = (productId: string) => {
navigate(`/products/${productId}`, {
state: { from: 'list' },
});
};
return <div>...</div>;
}📌 6. Route Loaders (v6.4+)
typescript
import {
createBrowserRouter,
RouterProvider,
useLoaderData,
} from 'react-router-dom';
// Define loader
async function userLoader({ params }: LoaderFunctionArgs) {
const user = await fetchUser(params.id!);
if (!user) {
throw new Response('Not Found', { status: 404 });
}
return user;
}
// Create router with loaders
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
path: 'users/:id',
element: <UserDetail />,
loader: userLoader,
errorElement: <ErrorPage />,
},
],
},
]);
// Component using loader data
function UserDetail() {
const user = useLoaderData() as User;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// App
function App() {
return <RouterProvider router={router} />;
}📦 XI. FORM HANDLING
📌 1. Controlled Components
typescript
type FormData = {
username: string;
email: string;
password: string;
};
function RegistrationForm() {
const [formData, setFormData] = useState<FormData>({
username: '',
email: '',
password: '',
});
const [errors, setErrors] = useState<
Partial<Record<keyof FormData, string>>
>({});
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
// Clear error khi user đang typing
if (errors[name as keyof FormData]) {
setErrors((prev) => ({
...prev,
[name]: undefined,
}));
}
};
const validate = (): boolean => {
const newErrors: Partial<Record<keyof FormData, string>> = {};
if (!formData.username.trim()) {
newErrors.username = 'Username is required';
}
if (!formData.email.includes('@')) {
newErrors.email = 'Invalid email';
}
if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!validate()) return;
try {
await registerUser(formData);
// Success handling
} catch (error) {
// Error handling
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Username</label>
<input
name='username'
value={formData.username}
onChange={handleChange}
/>
{errors.username && (
<span className='error'>{errors.username}</span>
)}
</div>
<div>
<label>Email</label>
<input
name='email'
type='email'
value={formData.email}
onChange={handleChange}
/>
{errors.email && <span className='error'>{errors.email}</span>}
</div>
<div>
<label>Password</label>
<input
name='password'
type='password'
value={formData.password}
onChange={handleChange}
/>
{errors.password && (
<span className='error'>{errors.password}</span>
)}
</div>
<button type='submit'>Register</button>
</form>
);
}📌 2. Uncontrolled Components with Refs
typescript
function UncontrolledForm() {
const usernameRef = useRef<HTMLInputElement>(null);
const emailRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
const formData = {
username: usernameRef.current?.value,
email: emailRef.current?.value,
};
console.log(formData);
};
return (
<form onSubmit={handleSubmit}>
<input ref={usernameRef} name='username' />
<input ref={emailRef} name='email' type='email' />
<button type='submit'>Submit</button>
</form>
);
}📌 3. Form with File Upload
typescript
function FileUploadForm() {
const [file, setFile] = useState<File | null>(null);
const [preview, setPreview] = useState<string>('');
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0];
if (selectedFile) {
setFile(selectedFile);
// Create preview
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result as string);
};
reader.readAsDataURL(selectedFile);
}
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!file) return;
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'My file');
try {
await fetch('/api/upload', {
method: 'POST',
body: formData,
});
} catch (error) {
console.error('Upload failed:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<input type='file' accept='image/*' onChange={handleFileChange} />
{preview && <img src={preview} alt='Preview' />}
<button type='submit' disabled={!file}>
Upload
</button>
</form>
);
}📌 4. Multi-Step Form
typescript
type Step1Data = { name: string; email: string };
type Step2Data = { address: string; city: string };
type Step3Data = { cardNumber: string };
type FormData = Step1Data & Step2Data & Step3Data;
function MultiStepForm() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState<Partial<FormData>>({});
const updateFormData = (data: Partial<FormData>) => {
setFormData((prev) => ({ ...prev, ...data }));
};
const nextStep = () => setStep((prev) => prev + 1);
const prevStep = () => setStep((prev) => prev - 1);
const handleSubmit = async () => {
try {
await submitForm(formData as FormData);
} catch (error) {
console.error('Submission failed:', error);
}
};
return (
<div>
{/* Progress indicator */}
<div className='progress'>Step {step} of 3</div>
{/* Steps */}
{step === 1 && (
<Step1
data={formData}
onNext={(data) => {
updateFormData(data);
nextStep();
}}
/>
)}
{step === 2 && (
<Step2
data={formData}
onNext={(data) => {
updateFormData(data);
nextStep();
}}
onBack={prevStep}
/>
)}
{step === 3 && (
<Step3
data={formData}
onSubmit={(data) => {
updateFormData(data);
handleSubmit();
}}
onBack={prevStep}
/>
)}
</div>
);
}
// Step components
function Step1({ data, onNext }: Step1Props) {
const [formData, setFormData] = useState<Step1Data>({
name: data.name || '',
email: data.email || '',
});
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
onNext(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
placeholder='Name'
/>
<input
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
placeholder='Email'
type='email'
/>
<button type='submit'>Next</button>
</form>
);
}🌐 XII. DATA FETCHING
📌 1. Fetch với useState & useEffect
typescript
type User = {
id: number;
name: string;
email: string;
};
function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
async function fetchUsers() {
try {
setLoading(true);
const response = await fetch('/api/users', {
signal: controller.signal,
});
if (!response.ok) {
throw new Error('Failed to fetch');
}
const data = await response.json();
setUsers(data);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
}
fetchUsers();
// EDGE CASE: Cleanup để cancel request khi unmount
return () => {
controller.abort();
};
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}📌 2. Custom Fetch Hook
typescript
type UseFetchResult<T> = {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
};
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [refetchIndex, setRefetchIndex] = useState(0);
const refetch = useCallback(() => {
setRefetchIndex((prev) => prev + 1);
}, []);
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
setLoading(true);
setError(null);
const response = await fetch(url, {
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => {
controller.abort();
};
}, [url, refetchIndex]);
return { data, loading, error, refetch };
}
// Usage
function Users() {
const { data, loading, error, refetch } = useFetch<User[]>('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<button onClick={refetch}>Refresh</button>
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}📌 3. Pagination
typescript
type PaginationState = {
page: number;
pageSize: number;
totalPages: number;
totalItems: number;
};
function usePagination<T>(
fetchFn: (
page: number,
pageSize: number
) => Promise<{
items: T[];
totalPages: number;
totalItems: number;
}>,
initialPageSize = 10
) {
const [items, setItems] = useState<T[]>([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState<PaginationState>({
page: 1,
pageSize: initialPageSize,
totalPages: 0,
totalItems: 0,
});
const fetchPage = useCallback(
async (page: number) => {
try {
setLoading(true);
const result = await fetchFn(page, pagination.pageSize);
setItems(result.items);
setPagination((prev) => ({
...prev,
page,
totalPages: result.totalPages,
totalItems: result.totalItems,
}));
} catch (error) {
console.error('Fetch failed:', error);
} finally {
setLoading(false);
}
},
[fetchFn, pagination.pageSize]
);
useEffect(() => {
fetchPage(1);
}, [fetchPage]);
const goToPage = (page: number) => {
if (page >= 1 && page <= pagination.totalPages) {
fetchPage(page);
}
};
const nextPage = () => goToPage(pagination.page + 1);
const prevPage = () => goToPage(pagination.page - 1);
return {
items,
loading,
pagination,
goToPage,
nextPage,
prevPage,
};
}
// Usage
function ProductList() {
const { items, loading, pagination, nextPage, prevPage } = usePagination(
async (page, pageSize) => {
const response = await fetch(
`/api/products?page=${page}&pageSize=${pageSize}`
);
return response.json();
}
);
return (
<div>
{loading && <div>Loading...</div>}
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<div className='pagination'>
<button onClick={prevPage} disabled={pagination.page === 1}>
Previous
</button>
<span>
Page {pagination.page} of {pagination.totalPages}
</span>
<button
onClick={nextPage}
disabled={pagination.page === pagination.totalPages}
>
Next
</button>
</div>
</div>
);
}📌 4. Infinite Scroll
typescript
function useInfiniteScroll<T>(
fetchFn: (page: number) => Promise<T[]>,
options = { threshold: 0.8 }
) {
const [items, setItems] = useState<T[]>([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const loaderRef = useRef<HTMLDivElement>(null);
const loadMore = useCallback(async () => {
if (loading || !hasMore) return;
try {
setLoading(true);
const newItems = await fetchFn(page);
if (newItems.length === 0) {
setHasMore(false);
} else {
setItems((prev) => [...prev, ...newItems]);
setPage((prev) => prev + 1);
}
} catch (error) {
console.error('Load more failed:', error);
} finally {
setLoading(false);
}
}, [fetchFn, page, loading, hasMore]);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
loadMore();
}
},
{ threshold: options.threshold }
);
if (loaderRef.current) {
observer.observe(loaderRef.current);
}
return () => {
observer.disconnect();
};
}, [loadMore, options.threshold]);
return { items, loading, hasMore, loaderRef };
}
// Usage
function InfiniteList() {
const { items, loading, hasMore, loaderRef } = useInfiniteScroll(
async (page) => {
const response = await fetch(`/api/items?page=${page}`);
return response.json();
}
);
return (
<div>
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
{hasMore && (
<div ref={loaderRef}>
{loading ? 'Loading...' : 'Load more'}
</div>
)}
</div>
);
}📌 5. Optimistic Updates
typescript
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = async (text: string) => {
// Temporary ID cho optimistic update
const tempId = `temp-${Date.now()}`;
const newTodo: Todo = {
id: tempId,
text,
completed: false,
};
// OPTIMISTIC: Update UI ngay lập tức
setTodos((prev) => [...prev, newTodo]);
try {
// Gọi API
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text }),
headers: { 'Content-Type': 'application/json' },
});
const savedTodo = await response.json();
// Replace temp todo với real todo
setTodos((prev) =>
prev.map((todo) => (todo.id === tempId ? savedTodo : todo))
);
} catch (error) {
// ROLLBACK: Remove temp todo nếu fail
setTodos((prev) => prev.filter((todo) => todo.id !== tempId));
console.error('Failed to add todo:', error);
}
};
const toggleTodo = async (id: string) => {
// OPTIMISTIC: Toggle ngay
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
try {
await fetch(`/api/todos/${id}/toggle`, { method: 'PATCH' });
} catch (error) {
// ROLLBACK: Toggle lại nếu fail
setTodos((prev) =>
prev.map((todo) =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
console.error('Failed to toggle todo:', error);
}
};
return (
<div>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type='checkbox'
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
</div>
);
}🎨 XIII. ANIMATIONS
📌 1. CSS Transitions
typescript
function FadeIn({ children }: { children: ReactNode }) {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
setIsVisible(true);
}, []);
return (
<div
style={{
opacity: isVisible ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
>
{children}
</div>
);
}📌 2. Conditional Rendering với Animation
typescript
function Modal({ isOpen, onClose, children }: ModalProps) {
const [shouldRender, setShouldRender] = useState(isOpen);
useEffect(() => {
if (isOpen) {
setShouldRender(true);
}
}, [isOpen]);
const handleAnimationEnd = () => {
if (!isOpen) {
setShouldRender(false);
}
};
if (!shouldRender) return null;
return (
<div
className={`modal ${isOpen ? 'modal--open' : 'modal--closed'}`}
onAnimationEnd={handleAnimationEnd}
>
<div className='modal__backdrop' onClick={onClose} />
<div className='modal__content'>{children}</div>
</div>
);
}
// CSS
// .modal--open { animation: fadeIn 0.3s; }
// .modal--closed { animation: fadeOut 0.3s; }📌 3. List Animations
typescript
function AnimatedList({ items }: { items: Item[] }) {
return (
<ul>
{items.map((item, index) => (
<li
key={item.id}
style={{
animation: `slideIn 0.3s ease-out ${index * 0.1}s both`,
}}
>
{item.name}
</li>
))}
</ul>
);
}
// CSS
// @keyframes slideIn {
// from {
// opacity: 0;
// transform: translateY(20px);
// }
// to {
// opacity: 1;
// transform: translateY(0);
// }
// }🎯 XIV. ACCESSIBILITY (A11Y)
📌 1. Keyboard Navigation
typescript
function Dropdown({ items, onSelect }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(-1);
const buttonRef = useRef<HTMLButtonElement>(null);
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex((prev) =>
prev < items.length - 1 ? prev + 1 : prev
);
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
break;
case 'Enter':
e.preventDefault();
if (selectedIndex >= 0) {
onSelect(items[selectedIndex]);
setIsOpen(false);
}
break;
case 'Escape':
setIsOpen(false);
buttonRef.current?.focus();
break;
}
};
return (
<div onKeyDown={handleKeyDown}>
<button
ref={buttonRef}
onClick={() => setIsOpen(!isOpen)}
aria-haspopup='listbox'
aria-expanded={isOpen}
>
Select Item
</button>
{isOpen && (
<ul role='listbox' tabIndex={-1}>
{items.map((item, index) => (
<li
key={item.id}
role='option'
aria-selected={index === selectedIndex}
onClick={() => {
onSelect(item);
setIsOpen(false);
}}
>
{item.name}
</li>
))}
</ul>
)}
</div>
);
}📌 2. Focus Management
typescript
function Dialog({ isOpen, onClose, children }: DialogProps) {
const dialogRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
// Save current focus
previousFocusRef.current = document.activeElement as HTMLElement;
// Focus dialog
dialogRef.current?.focus();
// Trap focus
const handleTab = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
const focusableElements = dialogRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (!focusableElements || focusableElements.length === 0)
return;
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[
focusableElements.length - 1
] as HTMLElement;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
};
document.addEventListener('keydown', handleTab);
return () => {
document.removeEventListener('keydown', handleTab);
// Restore focus
previousFocusRef.current?.focus();
};
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div ref={dialogRef} role='dialog' aria-modal='true' tabIndex={-1}>
<button onClick={onClose} aria-label='Close dialog'>
×
</button>
{children}
</div>
);
}📌 3. ARIA Labels & Roles
typescript
function SearchInput() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
return (
<div role='search'>
<label htmlFor='search-input'>Search</label>
<input
id='search-input'
type='text'
value={query}
onChange={(e) => setQuery(e.target.value)}
aria-label='Search'
aria-describedby='search-instructions'
aria-busy={isLoading}
/>
<div id='search-instructions' className='sr-only'>
Type to search. Results will appear below.
</div>
{results.length > 0 && (
<ul role='list' aria-label='Search results'>
{results.map((result, index) => (
<li key={index}>{result}</li>
))}
</ul>
)}
{isLoading && (
<div role='status' aria-live='polite'>
Loading results...
</div>
)}
</div>
);
}🔍 XV. DEBUGGING & DEV TOOLS
📌 1. Custom DevTools Hook
typescript
function useDebug(componentName: string, props: any) {
useEffect(() => {
console.group(`${componentName} - Props`);
console.log(props);
console.groupEnd();
}, [componentName, props]);
useEffect(() => {
console.log(`${componentName} mounted`);
return () => {
console.log(`${componentName} unmounted`);
};
}, [componentName]);
}
// Usage
function MyComponent(props: Props) {
useDebug('MyComponent', props);
return <div>...</div>;
}📌 2. Why Did You Render Hook
typescript
function useWhyDidYouUpdate(name: string, props: Record<string, any>) {
const previousProps = useRef<Record<string, any>>();
useEffect(() => {
if (previousProps.current) {
const allKeys = Object.keys({ ...previousProps.current, ...props });
const changedProps: Record<string, any> = {};
allKeys.forEach((key) => {
if (previousProps.current![key] !== props[key]) {
changedProps[key] = {
from: previousProps.current![key],
to: props[key],
};
}
});
if (Object.keys(changedProps).length > 0) {
console.log('[why-did-you-update]', name, changedProps);
}
}
previousProps.current = props;
});
}📌 3. Performance Profiling
typescript
function useProfiler(componentName: string) {
const renderCount = useRef(0);
const renderTime = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
useEffect(() => {
const start = performance.now();
return () => {
const end = performance.now();
renderTime.current = end - start;
console.log(`${componentName} Render #${renderCount.current}:`, {
time: `${renderTime.current.toFixed(2)}ms`,
});
};
});
}🏁 XVI. TÓM TẮT BEST PRACTICES
✅ NÊN LÀM (DO's)
Quản Lý State
- Nhóm các state có liên quan
- Dùng functional updates khi cần giá trị state trước đó
- Tránh lưu derived state
- Memoize các phép tính tốn tài nguyên
Hiệu Năng
- Dùng React.memo cho các component nặng
- Memoize callback bằng useCallback
- Memoize giá trị bằng useMemo
- Tách code (code splitting) cho ứng dụng lớn
Effects
- Luôn cleanup side effects
- Khai báo đúng dependencies
- Tách mối quan tâm (dùng nhiều useEffect)
- Dùng useLayoutEffect cho đo đạc DOM
Components
- Giữ component nhỏ gọn, tập trung một nhiệm vụ
- Tách logic tái sử dụng vào custom hooks
- Dùng compound components cho UI liên quan
- Khai báo kiểu TypeScript đầy đủ, chính xác
Forms
- Validate khi blur
- Xóa lỗi khi giá trị thay đổi
- Cung cấp thông báo lỗi rõ ràng, hữu ích
- Disable submit khi form không hợp lệ
Khả Năng Truy Cập (Accessibility)
- Dùng Semantic HTML
- Thêm ARIA labels khi cần
- Hỗ trợ điều hướng bằng bàn phím
- Quản lý focus hợp lý
❌ KHÔNG NÊN LÀM (DON'Ts)
Quản Lý State
- ❌ Không lưu derived state
- ❌ Không mutate state trực tiếp
- ❌ Không lạm dụng useState
Hiệu Năng
- ❌ Không tối ưu quá sớm (premature optimization)
- ❌ Không memoize mọi thứ
- ❌ Không tạo object/array trực tiếp trong JSX
Effects
- ❌ Không bỏ qua dependencies
- ❌ Không gọi setState trực tiếp trong render
- ❌ Không fetch data trong useLayoutEffect
Components
- ❌ Không tạo component quá lớn
- ❌ Không prop drilling quá sâu
- ❌ Không dùng inline functions trong JSX (nếu component nhạy cảm về hiệu năng)
📚 XVII. COMMON PATTERNS CHEAT SHEET
typescript
// 1. Conditional Rendering
{condition && <Component />}
{condition ? <A /> : <B />}
// 2. List Rendering
{items.map(item => <Item key={item.id} {...item} />)}
// 3. Event Handling
<button onClick={handleClick}>Click</button>
<input onChange={e => setValue(e.target.value)} />
// 4. Controlled Input
<input value={value} onChange={e => setValue(e.target.value)} />
// 5. Refs
const ref = useRef<HTMLDivElement>(null);
<div ref={ref} />
// 6. Context
const value = useContext(MyContext);
// 7. Custom Hook
function useCustom() {
const [state, setState] = useState();
return { state, setState };
}
// 8. Memoization
const value = useMemo(() => expensive(), [dep]);
const fn = useCallback(() => {}, [dep]);
// 9. Effect
useEffect(() => {
// side effect
return () => {
// cleanup
};
}, [dependencies]);
// 10. Lazy Loading
const Component = lazy(() => import('./Component'));
<Suspense fallback={<Loading />}>
<Component />
</Suspense>🎓 XVIII. MẸO PHỎNG VẤN
Câu Hỏi Thường Gặp
Virtual DOM là gì?
- Bản sao nhẹ của DOM thật
- React so sánh các thay đổi trên Virtual DOM
- Gộp (batch) các cập nhật để tối ưu hiệu năng
Reconciliation là gì?
- Quá trình React cập nhật DOM
- Thuật toán so sánh (diffing)
- Keys rất quan trọng khi render danh sách
useState vs useReducer?
- useState: state đơn giản
- useReducer: logic state phức tạp, nhiều cập nhật liên quan
useEffect vs useLayoutEffect?
- useEffect: chạy sau khi render ra màn hình (bất đồng bộ)
- useLayoutEffect: chạy trước khi render (đồng bộ, chặn render)
Làm sao để tối ưu ứng dụng React?
- React.memo, useMemo, useCallback
- Tách code (code splitting)
- Tải lười (lazy loading)
- Virtualization cho danh sách dài
Context vs Redux?
- Context: có sẵn trong React, chia sẻ state đơn giản
- Redux: mạnh mẽ, dễ dự đoán, có DevTools, middleware
Controlled vs Uncontrolled?
- Controlled: input được điều khiển bởi state của React
- Uncontrolled: DOM tự quản lý state
Keys trong danh sách?
- Giúp React xác định từng phần tử
- Phải ổn định và duy nhất
- Không dùng index nếu danh sách có thay đổi thứ tự
Workflow này bao phủm toàn bộ kiến thức ReactJS từ Basic đến Advanced/Senior level.
Các chủ đề đã cover:
- ✅ State Management (useState, useReducer, Context)
- ✅ Performance (useMemo, useCallback, React.memo)
- ✅ Side Effects (useEffect, useLayoutEffect)
- ✅ Refs (useRef, useImperativeHandle)
- ✅ Component Patterns (Compound, Render Props, HOC)
- ✅ Routing (React Router)
- ✅ Forms (Controlled, Multi-step, File upload)
- ✅ Data Fetching (Custom hooks, Pagination, Infinite scroll)
- ✅ Animations
- ✅ Accessibility
- ✅ TypeScript Patterns
- ✅ Testing
- ✅ Debugging
- ✅ Best Practices
- ✅ Interview Prep
Áp dụng workflow này cho React để đảm bảo code quality cao nhất! 🚀