React Patterns
Higher-Order Components (HOC)
HOC là function nhận một component và trả về component mới với behavior được thêm vào.
// withLoading HOC
function withLoading<T extends object>(
WrappedComponent: React.ComponentType<T>
) {
return function WithLoadingComponent({
isLoading,
...props
}: T & { isLoading: boolean }) {
if (isLoading) {
return <div className="spinner">Loading...</div>;
}
return <WrappedComponent {...(props as T)} />;
};
}
// withAuth HOC
function withAuth<T extends object>(WrappedComponent: React.ComponentType<T>) {
return function WithAuthComponent(props: T) {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return <WrappedComponent {...props} />;
};
}
// withErrorBoundary HOC
function withErrorBoundary<T extends object>(
WrappedComponent: React.ComponentType<T>,
fallback: React.ReactNode
) {
return function WithErrorBoundary(props: T) {
return (
<ErrorBoundary fallback={fallback}>
<WrappedComponent {...props} />
</ErrorBoundary>
);
};
}
// Kết hợp nhiều HOCs
const ProtectedDashboard = withAuth(withLoading(Dashboard));
// Sử dụng
<ProtectedDashboard isLoading={loading} data={data} />
Compound Components
Pattern cho phép components chia sẻ state ngầm, API linh hoạt như HTML (select/option).
import { createContext, useContext, useState } from 'react';
// Tabs compound component
interface TabsContextType {
activeTab: string;
setActiveTab: (id: string) => void;
}
const TabsContext = createContext<TabsContextType | null>(null);
function useTabs() {
const ctx = useContext(TabsContext);
if (!ctx) throw new Error('Must be used within Tabs');
return ctx;
}
// Parent component giữ state
function Tabs({
defaultTab,
children,
}: {
defaultTab: string;
children: React.ReactNode;
}) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
// Sub-components
function TabList({ children }: { children: React.ReactNode }) {
return <div className="tab-list" role="tablist">{children}</div>;
}
function Tab({ id, children }: { id: string; children: React.ReactNode }) {
const { activeTab, setActiveTab } = useTabs();
return (
<button
role="tab"
aria-selected={activeTab === id}
className={activeTab === id ? 'tab active' : 'tab'}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
}
function TabPanels({ children }: { children: React.ReactNode }) {
return <div className="tab-panels">{children}</div>;
}
function TabPanel({ id, children }: { id: string; children: React.ReactNode }) {
const { activeTab } = useTabs();
if (activeTab !== id) return null;
return (
<div role="tabpanel" className="tab-panel">
{children}
</div>
);
}
// Gắn vào Tabs
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panels = TabPanels;
Tabs.Panel = TabPanel;
// Sử dụng - API rất tự nhiên
function App() {
return (
<Tabs defaultTab="overview">
<Tabs.List>
<Tabs.Tab id="overview">Overview</Tabs.Tab>
<Tabs.Tab id="analytics">Analytics</Tabs.Tab>
<Tabs.Tab id="settings">Settings</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel id="overview"><Overview /></Tabs.Panel>
<Tabs.Panel id="analytics"><Analytics /></Tabs.Panel>
<Tabs.Panel id="settings"><Settings /></Tabs.Panel>
</Tabs.Panels>
</Tabs>
);
}
Portals
Render component vào DOM node bên ngoài component tree.
import { createPortal } from 'react-dom';
import { useEffect, useRef } from 'react';
// Modal với Portal
function Modal({
isOpen,
onClose,
title,
children,
}: {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}) {
const overlayRef = useRef<HTMLDivElement>(null);
// Close khi click overlay
function handleOverlayClick(e: React.MouseEvent) {
if (e.target === overlayRef.current) {
onClose();
}
}
// Close khi nhấn Escape
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') onClose();
}
if (isOpen) {
document.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden'; // Ngăn scroll
}
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = '';
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return createPortal(
<div
ref={overlayRef}
className="modal-overlay"
onClick={handleOverlayClick}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div className="modal-content">
<div className="modal-header">
<h2 id="modal-title">{title}</h2>
<button onClick={onClose} aria-label="Close modal">×</button>
</div>
<div className="modal-body">{children}</div>
</div>
</div>,
document.body // Render trực tiếp vào body
);
}
// Sử dụng
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)} title="Confirm">
<p>Are you sure?</p>
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={handleConfirm}>Confirm</button>
</Modal>
</div>
);
}
Error Boundaries
Bắt JavaScript errors trong component tree, display fallback UI.
// ErrorBoundary phải là Class Component
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
fallback: ReactNode | ((error: Error) => ReactNode);
onError?: (error: Error, info: ErrorInfo) => void;
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.onError?.(error, info);
// Log to error reporting service
console.error('Error boundary caught:', error, info);
}
render() {
if (this.state.hasError && this.state.error) {
const { fallback } = this.props;
return typeof fallback === 'function'
? fallback(this.state.error)
: fallback;
}
return this.props.children;
}
}
// Sử dụng
function App() {
return (
<ErrorBoundary
fallback={(error) => (
<div className="error-page">
<h1>Something went wrong</h1>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>Reload</button>
</div>
)}
onError={(error) => logErrorToService(error)}
>
<Dashboard />
</ErrorBoundary>
);
}
Render Props Pattern
// Mouse tracker với render props
function MouseTracker({
render,
}: {
render: (pos: { x: number; y: number }) => ReactNode;
}) {
const [position, setPosition] = useState({ x: 0, y: 0 });
return (
<div
onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}
style={{ height: '300px', border: '1px solid #ccc' }}
>
{render(position)}
</div>
);
}
// Sử dụng
<MouseTracker
render={({ x, y }) => (
<p>
Mouse position: ({x}, {y})
</p>
)}
/>
Lưu ý: Ngày nay Custom Hooks thường thay thế Render Props và HOCs vì code gọn hơn.