React membuka paradigma baru dalam membangun antarmuka pengguna, dimana aplikasi dipandang sebagai komposisi komponen kecil yang re usable, bukan monolith monolithic script yang tricky dan susah di-maintain.
Sejak kemunculannya, ekosistem React telah berkembang pesat, membawa banyak pola, teknik arsitektur, dan best practice yang membantu developer menulis kode yang lebih bersih, lebih mudah dikelola, dan lebih andal. Pola-pola ini tidak terbatas pada React core melainkan mencakup juga bagaimana berinteraksi dengan state, data fetching, optimasi performa, testing, dan integrasi dengan tool modern seperti TypeScript, Zustand, TanStack Query, dan lainnya.
Artikel ini akan menjadi referensi, mengumpulkan pattern yang paling fundamental hingga yang paling modern, lengkap dengan contoh kode, kapan pattern tersebut cocok digunakan, serta beberapa anti-pattern untuk menghindari re-inventing wheel yang buruk.
Daftar Isi
- Core Component Patterns
- State Management Patterns
- Data Fetching Patterns
- Composition Patterns
- Hook Patterns
- Rendering Patterns
- Performance Patterns
- Architecture & Structure Patterns
- Form Patterns
- Testing Patterns
- Accessibility Patterns
- React 18 / 19 Modern Patterns
- Prinsip & Filosofi
- Anti-Patterns (Yang Harus Dihindari)
1. Core Component Patterns
Pattern dasar yang menjadi fondasi cara berpikir di React.
1.1 Component Composition Pattern

Aplikasi bukan monolith, melainkan komposisi ratusan komponen yang bekerja bersama. Tiap komponen punya satu alasan untuk ada dan bisa digunakan oleh komponen lain.
// Buruk — satu komponen monolith yang melakukan segalanya
const Dashboard = () => (
<div>
{/* ratusan baris JSX, logic, fetch, dll */}
</div>
)
// Baik — komposisi komponen kecil yang focused
const Dashboard = () => (
<DashboardLayout>
<Sidebar />
<main>
<MetricsBar />
<RecentActivity />
<QuickActions />
</main>
</DashboardLayout>
)
Kapan digunakan: Selalu. Ini fondasi dari semua React development.
1.2 Container / Presentational Pattern
Pisahkan komponen menjadi dua peran: Container (logika + data) dan Presentational (hanya render props yang diterima).
// Presentational — hanya render, tidak tahu asal data
const UserCard = ({ name, email, avatarUrl, onEdit }: UserCardProps) => (
<div className="card">
<img src={avatarUrl} alt={name} />
<h3>{name}</h3>
<p>{email}</p>
<button onClick={onEdit}>Edit</button>
</div>
)
// Container — kelola data dan logika
const UserCardContainer = ({ userId }: { userId: string }) => {
const { user, isLoading } = useUser(userId)
const navigate = useNavigate()
if (isLoading) return <Skeleton />
if (!user) return null
return (
<UserCard
name={user.name}
email={user.email}
avatarUrl={user.avatar}
onEdit={() => navigate(`/users/${userId}/edit`)}
/>
)
}
Kapan digunakan: Saat membangun komponen yang butuh reusability tinggi. Komponen presentational bisa di-test murni tanpa mock API.
1.3 Controlled vs Uncontrolled Component Pattern
Tentukan apakah state dikelola oleh komponen itu sendiri (uncontrolled) atau oleh parent-nya (controlled).
// Uncontrolled — kelola state sendiri
const UncontrolledToggle = () => {
const [isOn, setIsOn] = useState(false)
return <Switch checked={isOn} onChange={() => setIsOn(v => !v)} />
}
// Controlled — state dari parent
const ControlledToggle = ({
value,
onChange
}: {
value: boolean
onChange: (val: boolean) => void
}) => (
<Switch checked={value} onChange={() => onChange(!value)} />
)
// Flexible — support keduanya (pola library UI)
const FlexibleToggle = ({
defaultValue,
value: controlledValue,
onChange
}: FlexibleToggleProps) => {
const [internalValue, setInternalValue] = useState(defaultValue ?? false)
const isControlled = controlledValue !== undefined
const value = isControlled ? controlledValue : internalValue
const handleChange = (newVal: boolean) => {
if (!isControlled) setInternalValue(newVal)
onChange?.(newVal)
}
return <Switch checked={value} onChange={() => handleChange(!value)} />
}
Kapan digunakan: Controlled untuk form dan state yang perlu dikontrol parent. Uncontrolled untuk komponen yang berdiri sendiri. Flexible untuk library komponen reusable.
1.4 Compound Components Pattern
Sekumpulan komponen yang bekerja bersama sebagai satu entitas, berbagi state implisit melalui Context tanpa prop drilling.
// State shared via Context
const TabsContext = createContext<TabsContextType | null>(null)
const useTabs = () => {
const ctx = useContext(TabsContext)
if (!ctx) throw new Error('Must be used inside <Tabs>')
return ctx
}
// Root component — penyedia state
const Tabs = ({ defaultTab, children }: TabsProps) => {
const [activeTab, setActiveTab] = useState(defaultTab)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
)
}
// Sub-components
Tabs.List = ({ children }: { children: ReactNode }) => (
<div role="tablist" className="tabs-list">{children}</div>
)
Tabs.Tab = ({ id, children }: TabProps) => {
const { activeTab, setActiveTab } = useTabs()
return (
<button
role="tab"
aria-selected={activeTab === id}
onClick={() => setActiveTab(id)}
>
{children}
</button>
)
}
Tabs.Panel = ({ id, children }: PanelProps) => {
const { activeTab } = useTabs()
return activeTab === id ? <div role="tabpanel">{children}</div> : null
}
// Penggunaan — sangat readable
const App = () => (
<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.Panel id="overview"><Overview /></Tabs.Panel>
<Tabs.Panel id="analytics"><Analytics /></Tabs.Panel>
<Tabs.Panel id="settings"><Settings /></Tabs.Panel>
</Tabs>
)
Kapan digunakan: Membangun komponen UI kompleks yang sangat customizable: Tabs, Accordion, Select, Modal, Menu. Sering ditemukan di Radix UI, Headless UI, Reach UI.
1.5 Headless Component Pattern
Komponen yang menyediakan logika dan behavior tanpa opini tentang tampilan. Consumer sepenuhnya mengontrol styling dan markup.
// Headless hook — semua logika, tanpa UI
const useAccordion = (items: AccordionItem[]) => {
const [openItems, setOpenItems] = useState<Set<string>>(new Set())
const toggle = (id: string) => {
setOpenItems(prev => {
const next = new Set(prev)
next.has(id) ? next.delete(id) : next.add(id)
return next
})
}
const isOpen = (id: string) => openItems.has(id)
const getItemProps = (id: string) => ({
'aria-expanded': isOpen(id),
onClick: () => toggle(id),
})
const getPanelProps = (id: string) => ({
role: 'region' as const,
hidden: !isOpen(id),
})
return { isOpen, getItemProps, getPanelProps }
}
// Consumer bebas styling sepenuhnya
const MyAccordion = ({ items }: { items: AccordionItem[] }) => {
const { isOpen, getItemProps, getPanelProps } = useAccordion(items)
return (
<div className="my-custom-accordion">
{items.map(item => (
<div key={item.id} className="accordion-item">
<button
className={`trigger ${isOpen(item.id) ? 'open' : ''}`}
{...getItemProps(item.id)}
>
{item.title}
<ChevronIcon rotated={isOpen(item.id)} />
</button>
<div className="panel" {...getPanelProps(item.id)}>
{item.content}
</div>
</div>
))}
</div>
)
}
Kapan digunakan: Membangun design system yang butuh fleksibilitas styling penuh. Library seperti Radix UI, TanStack Table, dan Downshift menggunakan pendekatan ini.
1.6 Provider Pattern
Bagikan data atau behavior ke seluruh subtree komponen tanpa prop drilling, menggunakan React Context.
// Contoh lengkap dengan custom hook
type AuthContextType = {
user: User | null
login: (credentials: Credentials) => Promise<void>
logout: () => void
isAuthenticated: boolean
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null)
const login = async (credentials: Credentials) => {
const user = await authService.login(credentials)
setUser(user)
}
const logout = () => {
authService.logout()
setUser(null)
}
return (
<AuthContext.Provider
value={{ user, login, logout, isAuthenticated: !!user }}
>
{children}
</AuthContext.Provider>
)
}
// Custom hook — throw jika digunakan di luar provider
export const useAuth = () => {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error('useAuth must be inside AuthProvider')
return ctx
}
Kapan digunakan: Theme, locale/i18n, auth state, feature flags — data yang jarang berubah dan dibutuhkan banyak komponen. Jangan gunakan untuk state yang sering berubah (gunakan Zustand/Jotai).
1.7 Render Props Pattern
Berikan fungsi sebagai prop yang mengontrol apa yang akan di-render. Memberi consumer kendali penuh atas UI sambil reuse logika.
type MouseTrackerRenderProps = {
x: number
y: number
}
const MouseTracker = ({
render
}: {
render: (props: MouseTrackerRenderProps) => ReactNode
}) => {
const [position, setPosition] = useState({ x: 0, y: 0 })
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
setPosition({ x: e.clientX, y: e.clientY })
}
return (
<div onMouseMove={handleMouseMove} style={{ height: '100vh' }}>
{render(position)}
</div>
)
}
// Penggunaan dengan children as function (variasi populer)
const MouseTracker2 = ({
children
}: {
children: (props: MouseTrackerRenderProps) => ReactNode
}) => { /* sama */ }
const App = () => (
<MouseTracker
render={({ x, y }) => (
<div>Mouse: {x}, {y}</div>
)}
/>
)
Kapan digunakan: Saat perlu share logika tapi UI bisa sangat bervariasi. Custom hooks sering menggantikan ini, tapi render props masih relevan untuk kasus tertentu (especially library code).
1.8 Higher-Order Component (HOC) Pattern

Fungsi yang menerima komponen dan mengembalikan komponen baru dengan behavior tambahan. Pattern lama, sekarang mayoritas digantikan custom hooks.
// HOC klasik
function withAuth<T extends { user: User }>(
WrappedComponent: ComponentType<T>
) {
return function AuthenticatedComponent(
props: Omit<T, 'user'>
) {
const { user, isAuthenticated } = useAuth()
if (!isAuthenticated) return <Navigate to="/login" />
return <WrappedComponent {...(props as T)} user={user!} />
}
}
const ProtectedDashboard = withAuth(Dashboard)
// Ekuivalen modern dengan custom hook (lebih direkomendasikan)
const Dashboard = () => {
const { user } = useRequireAuth() // redirect jika belum auth
return <div>Welcome {user.name}</div>
}
Kapan digunakan: Legacy codebase, atau situasi spesifik seperti React.memo, React.forwardRef, dan error boundaries (yang masih butuh class component). Untuk code baru, sebaiknya menggunakan custom hooks.
1.9 Props Getter Pattern
Hook yang mengembalikan fungsi-fungsi untuk mendapatkan props yang diperlukan komponen, dengan kemampuan override. Digunakan oleh React Hook Form, Downshift.
const useCombobox = (items: string[]) => {
const [isOpen, setIsOpen] = useState(false)
const [inputValue, setInputValue] = useState('')
const [selectedItem, setSelectedItem] = useState<string | null>(null)
const filteredItems = items.filter(item =>
item.toLowerCase().includes(inputValue.toLowerCase())
)
// Props getter — bisa di-override oleh consumer
const getInputProps = (overrides = {}) => ({
value: inputValue,
onChange: (e: ChangeEvent<HTMLInputElement>) => setInputValue(e.target.value),
onFocus: () => setIsOpen(true),
'aria-autocomplete': 'list' as const,
'aria-expanded': isOpen,
...overrides,
})
const getMenuProps = (overrides = {}) => ({
role: 'listbox' as const,
'aria-label': 'Options',
...overrides,
})
const getItemProps = (item: string, overrides = {}) => ({
role: 'option' as const,
'aria-selected': selectedItem === item,
onClick: () => {
setSelectedItem(item)
setInputValue(item)
setIsOpen(false)
},
...overrides,
})
return { isOpen, filteredItems, selectedItem, getInputProps, getMenuProps, getItemProps }
}
// Consumer bisa override props sesuai kebutuhan
const Combobox = ({ items }: { items: string[] }) => {
const { isOpen, filteredItems, getInputProps, getMenuProps, getItemProps } =
useCombobox(items)
return (
<div>
<input
{...getInputProps({
placeholder: 'Search...',
className: 'search-input'
})}
/>
{isOpen && (
<ul {...getMenuProps({ className: 'menu' })}>
{filteredItems.map(item => (
<li key={item} {...getItemProps(item)}>
{item}
</li>
))}
</ul>
)}
</div>
)
}
Kapan digunakan: Membangun reusable components dengan banyak accessibility props, atau library komponen yang butuh flexibility tinggi.
1.10 Atomic Design Pattern
Hierarki komponen yang terinspirasi dari kimia: Atoms → Molecules → Organisms → Templates → Pages.
// Atom — unit terkecil, tidak punya dependency ke komponen lain
const Button = ({ children, variant = 'primary', onClick, disabled }: ButtonProps) => (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
)
const Input = ({ value, onChange, placeholder, error }: InputProps) => (
<div className="input-wrapper">
<input value={value} onChange={onChange} placeholder={placeholder} />
{error && <span className="error">{error}</span>}
</div>
)
// Molecule — kombinasi atom-atom
const SearchField = ({ onSearch }: { onSearch: (query: string) => void }) => {
const [query, setQuery] = useState('')
return (
<div className="search-field">
<Input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search..." />
<Button onClick={() => onSearch(query)}>
<SearchIcon />
</Button>
</div>
)
}
// Organism — UI section yang kompleks dan mandiri
const Header = () => (
<header className="header">
<Logo />
<SearchField onSearch={handleSearch} />
<NavMenu />
<UserAvatar />
</header>
)
// Template — layout tanpa data nyata
const DashboardTemplate = ({ header, sidebar, content, footer }: TemplateProps) => (
<div className="dashboard-layout">
{header}
<div className="body">
{sidebar}
<main>{content}</main>
</div>
{footer}
</div>
)
// Page — template + data nyata
const DashboardPage = () => (
<DashboardTemplate
header={<Header />}
sidebar={<Sidebar />}
content={<DashboardContent />}
footer={<Footer />}
/>
)
Kapan digunakan: Membangun design system yang besar dan konsisten. Cocok untuk tim yang perlu panduan struktur komponen yang jelas.
1.11 Error Boundary Pattern
Tangkap error JavaScript di subtree komponen dan tampilkan fallback UI, mencegah crash seluruh aplikasi.
// Class component — satu-satunya cara (belum ada hook untuk ini)
class ErrorBoundary extends React.Component<
{ fallback: ReactNode; onError?: (error: Error) => void; children: ReactNode },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null }
static getDerivedStateFromError(error: Error) {
return { hasError: true, error }
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.onError?.(error)
// Kirim ke error monitoring (Sentry, Datadog)
errorMonitoring.captureException(error, { extra: info })
}
render() {
if (this.state.hasError) {
return this.props.fallback
}
return this.props.children
}
}
// Wrapper fungsional untuk DX yang lebih baik
const withErrorBoundary = <T extends object>(
Component: ComponentType<T>,
fallback: ReactNode
) => {
return (props: T) => (
<ErrorBoundary fallback={fallback}>
<Component {...props} />
</ErrorBoundary>
)
}
// Penggunaan — isolasi tiap section independen
const App = () => (
<ErrorBoundary fallback={<AppCrashFallback />}>
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError />}>
<MainContent />
</ErrorBoundary>
</ErrorBoundary>
)
Kapan digunakan: Isolasi fitur-fitur yang independent. Pastikan ada minimal satu di root app, dan per-section untuk fitur kritis yang tidak boleh crash seluruh app.
1.12 Portal Pattern
Render komponen ke DOM node di luar parent hierarchy, berguna untuk overlay yang butuh escape dari CSS stacking context.
import { createPortal } from 'react-dom'
const Modal = ({ isOpen, onClose, title, children }: ModalProps) => {
// Trap focus saat modal buka
useEffect(() => {
if (!isOpen) return
const previouslyFocused = document.activeElement as HTMLElement
return () => previouslyFocused?.focus()
}, [isOpen])
if (!isOpen) return null
return createPortal(
<div
className="modal-backdrop"
onClick={onClose}
role="dialog"
aria-modal
aria-labelledby="modal-title"
>
<div
className="modal-content"
onClick={e => e.stopPropagation()}
>
<div className="modal-header">
<h2 id="modal-title">{title}</h2>
<button onClick={onClose} aria-label="Close">✕</button>
</div>
<div className="modal-body">{children}</div>
</div>
</div>,
document.getElementById('portal-root') ?? document.body
)
}
Kapan digunakan: Modal, drawer, tooltip, context menu, dropdown yang butuh break dari overflow: hidden atau z-index constraints parent.
2. State Management Patterns
Pattern untuk mengelola state di berbagai skala dan kompleksitas.

2.1 Local State Pattern
State yang hanya relevan untuk satu komponen, gunakan useState atau useReducer.
// useState untuk state sederhana
const Counter = () => {
const [count, setCount] = useState(0)
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
)
}
// useReducer untuk state kompleks dengan banyak aksi
type CartState = { items: CartItem[]; total: number }
type CartAction =
| { type: 'ADD_ITEM'; payload: CartItem }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'CLEAR' }
const cartReducer = (state: CartState, action: CartAction): CartState => {
switch (action.type) {
case 'ADD_ITEM':
return {
items: [...state.items, action.payload],
total: state.total + action.payload.price
}
case 'REMOVE_ITEM':
const item = state.items.find(i => i.id === action.payload)
return {
items: state.items.filter(i => i.id !== action.payload),
total: state.total - (item?.price ?? 0)
}
case 'CLEAR':
return { items: [], total: 0 }
}
}
const Cart = () => {
const [cart, dispatch] = useReducer(cartReducer, { items: [], total: 0 })
// ...
}
2.2 Lifted State Pattern
Angkat state ke ancestor terdekat yang dibutuhkan oleh multiple sibling components.
// State di-lift ke parent yang menjadi common ancestor
const ProductPage = () => {
const [selectedVariant, setSelectedVariant] = useState<Variant | null>(null)
return (
<div>
{/* Keduanya perlu tahu selectedVariant */}
<VariantSelector
selected={selectedVariant}
onSelect={setSelectedVariant}
/>
<PriceDisplay variant={selectedVariant} />
<AddToCartButton variant={selectedVariant} />
</div>
)
}
2.3 URL State Pattern
Simpan state di URL (query params, path) untuk shareable dan bookmarkable UI state.
import { useSearchParams } from 'react-router-dom'
const ProductList = () => {
const [searchParams, setSearchParams] = useSearchParams()
const filters = {
category: searchParams.get('category') ?? 'all',
sortBy: searchParams.get('sort') ?? 'newest',
page: Number(searchParams.get('page') ?? '1'),
}
const updateFilter = (key: string, value: string) => {
setSearchParams(prev => {
prev.set(key, value)
if (key !== 'page') prev.set('page', '1') // reset page on filter change
return prev
})
}
return (
<div>
<FilterBar filters={filters} onFilterChange={updateFilter} />
<ProductGrid filters={filters} />
<Pagination current={filters.page} onChange={p => updateFilter('page', String(p))} />
</div>
)
}
2.4 Global State Pattern (Zustand)
State yang perlu diakses dari mana saja tanpa prop drilling, tanpa overhead Context re-render.
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
type UserStore = {
user: User | null
token: string | null
setUser: (user: User, token: string) => void
logout: () => void
}
const useUserStore = create<UserStore>()(
persist(
(set) => ({
user: null,
token: null,
setUser: (user, token) => set({ user, token }),
logout: () => set({ user: null, token: null }),
}),
{ name: 'user-storage' }
)
)
// Gunakan di mana saja tanpa Provider
const Avatar = () => {
const user = useUserStore(s => s.user) // selector — hanya re-render jika user berubah
return <img src={user?.avatar} />
}
2.5 Derived State Pattern
Hitung state dari state yang sudah ada, jangan duplikasi state.
// BURUK — state yang bisa di-derive di-simpan terpisah
const [items, setItems] = useState<CartItem[]>([])
const [total, setTotal] = useState(0) // redundant! bisa di-hitung dari items
// BAIK — derive dari sumber kebenaran tunggal
const [items, setItems] = useState<CartItem[]>([])
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)
const hasItems = items.length > 0
// Untuk kalkulasi mahal, gunakan useMemo
const expensiveTotal = useMemo(
() => items.reduce((sum, item) => sum + calculateItemTotal(item), 0),
[items]
)
2.6 Optimistic Update Pattern
Update UI terlebih dahulu sebelum response server tiba, rollback jika gagal.
const useLikePost = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (postId: string) => api.likePost(postId),
// Update cache sebelum request selesai
onMutate: async (postId) => {
await queryClient.cancelQueries({ queryKey: ['posts'] })
const previous = queryClient.getQueryData(['posts'])
queryClient.setQueryData(['posts'], (old: Post[]) =>
old.map(p => p.id === postId ? { ...p, likes: p.likes + 1, liked: true } : p)
)
return { previous } // untuk rollback
},
// Rollback jika gagal
onError: (_, __, context) => {
queryClient.setQueryData(['posts'], context?.previous)
},
// Sinkronkan dengan server setelah selesai
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] })
},
})
}
3. Data Fetching Patterns
Pattern untuk mengambil, cache, dan synchronize data dengan server.

3.1 Custom Hook Fetching Pattern
Encapsulasi fetch logic ke dalam custom hook yang reusable.
const useUsers = (filters?: UserFilters) => {
return useQuery({
queryKey: ['users', filters],
queryFn: () => userService.getUsers(filters),
staleTime: 5 * 60 * 1000, // 5 menit
select: (data) => data.sort((a, b) => a.name.localeCompare(b.name)),
})
}
// Komponen sangat bersih
const UserList = () => {
const { data: users, isLoading, error } = useUsers({ role: 'admin' })
if (isLoading) return <Skeleton count={5} />
if (error) return <ErrorState message={error.message} />
return <ul>{users?.map(u => <UserRow key={u.id} user={u} />)}</ul>
}
3.2 Parallel Fetching Pattern
Jalankan multiple request secara bersamaan untuk mengurangi total waktu tunggu.
// Parallel dengan useQueries
const useDashboardData = () => {
return useQueries({
queries: [
{ queryKey: ['metrics'], queryFn: api.getMetrics },
{ queryKey: ['activity'], queryFn: api.getRecentActivity },
{ queryKey: ['alerts'], queryFn: api.getAlerts },
]
})
}
const Dashboard = () => {
const [metrics, activity, alerts] = useDashboardData()
return (
<div>
<MetricsSection data={metrics.data} loading={metrics.isLoading} />
<ActivityFeed data={activity.data} loading={activity.isLoading} />
<AlertBanner data={alerts.data} loading={alerts.isLoading} />
</div>
)
}
3.3 Prefetching Pattern
Fetch data sebelum user membutuhkannya — saat hover, saat idle, atau saat komponen parent render.
const useUserPrefetch = () => {
const queryClient = useQueryClient()
const prefetchUser = useCallback((userId: string) => {
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => api.getUser(userId),
staleTime: 60_000,
})
}, [queryClient])
return prefetchUser
}
// Prefetch saat hover — data sudah siap saat user klik
const UserRow = ({ user }: { user: User }) => {
const prefetchUser = useUserPrefetch()
return (
<li
onMouseEnter={() => prefetchUser(user.id)}
onFocus={() => prefetchUser(user.id)}
>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
)
}
3.4 Infinite Scroll / Pagination Pattern
Load data bertahap seiring user scroll atau klik "Load More".
const useInfinitePosts = (category: string) => {
return useInfiniteQuery({
queryKey: ['posts', category],
queryFn: ({ pageParam }) => api.getPosts({ category, cursor: pageParam }),
initialPageParam: null as string | null,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
}
const PostFeed = ({ category }: { category: string }) => {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfinitePosts(category)
const { ref } = useIntersectionObserver({
onChange: (inView) => { if (inView && hasNextPage) fetchNextPage() }
})
const posts = data?.pages.flatMap(page => page.posts) ?? []
return (
<div>
{posts.map(post => <PostCard key={post.id} post={post} />)}
<div ref={ref}>
{isFetchingNextPage && <Spinner />}
</div>
</div>
)
}
3.5 Stale-While-Revalidate Pattern
Tampilkan data lama (stale) sementara fetch data terbaru di background. UX terasa instan.
const useStaleProduct = (id: string) => {
return useQuery({
queryKey: ['product', id],
queryFn: () => api.getProduct(id),
staleTime: 30_000, // data dianggap fresh 30 detik
gcTime: 5 * 60_000, // cache disimpan 5 menit setelah tidak digunakan
refetchOnWindowFocus: true, // revalidate saat user kembali ke tab
placeholderData: keepPreviousData, // tampilkan data lama saat fetch baru
})
}
4. Composition Patterns
Pattern untuk menyusun komponen dengan lebih fleksibel.
4.1 Children as Props
Gunakan children untuk konten yang tidak perlu diketahui komponen wrapper.
const Card = ({
children,
className,
padding = 'md'
}: {
children: ReactNode
className?: string
padding?: 'sm' | 'md' | 'lg'
}) => (
<div className={clsx('card', `p-${padding}`, className)}>
{children}
</div>
)
// Consumer menentukan isi — Card tidak perlu tahu
const UserCard = ({ user }: { user: User }) => (
<Card padding="lg">
<Avatar src={user.avatar} />
<h3>{user.name}</h3>
<Badge>{user.role}</Badge>
</Card>
)
4.2 Slot Pattern (Named Children)
Versi lebih eksplisit dari children — multiple "slot" untuk bagian UI yang berbeda.
type PageLayoutProps = {
header?: ReactNode
sidebar?: ReactNode
children: ReactNode
footer?: ReactNode
}
const PageLayout = ({ header, sidebar, children, footer }: PageLayoutProps) => (
<div className="layout">
{header && <header className="layout-header">{header}</header>}
<div className="layout-body">
{sidebar && <aside className="layout-sidebar">{sidebar}</aside>}
<main className="layout-main">{children}</main>
</div>
{footer && <footer className="layout-footer">{footer}</footer>}
</div>
)
// Penggunaan
const App = () => (
<PageLayout
header={<TopNav />}
sidebar={<Sidebar />}
footer={<Footer />}
>
<DashboardContent />
</PageLayout>
)
4.3 Polymorphic Component Pattern
Komponen yang bisa merender sebagai elemen HTML yang berbeda melalui prop as.
type PolymorphicProps<T extends ElementType> = {
as?: T
children?: ReactNode
className?: string
} & ComponentPropsWithoutRef<T>
const Text = <T extends ElementType = 'span'>({
as,
children,
className,
...props
}: PolymorphicProps<T>) => {
const Component = as ?? 'span'
return (
<Component className={clsx('text', className)} {...props}>
{children}
</Component>
)
}
// Fleksibel — bisa render sebagai elemen apapun
<Text as="h1" className="title">Heading</Text>
<Text as="p" className="body">Paragraph</Text>
<Text as="label" htmlFor="email">Email</Text>
<Text as={Link} to="/profile">Profile</Text> // juga bisa component!
4.4 Component Injection Pattern
Injeksikan komponen sebagai prop untuk customisasi rendering.
type TableProps<T> = {
data: T[]
columns: Column<T>[]
LoadingComponent?: ComponentType
EmptyComponent?: ComponentType
RowComponent?: ComponentType<{ item: T }>
}
const Table = <T extends object>({
data,
columns,
LoadingComponent = DefaultLoading,
EmptyComponent = DefaultEmpty,
RowComponent = DefaultRow,
}: TableProps<T>) => {
// Consumer bisa inject custom component
}
// Penggunaan — custom row tanpa modifikasi Table
<Table
data={orders}
columns={orderColumns}
RowComponent={OrderRow}
EmptyComponent={NoOrdersState}
/>
5. Hook Patterns
Pattern untuk membangun dan menggunakan custom hooks yang efektif.
5.1 Custom Hook Pattern
Encapsulasi logika stateful yang reusable ke dalam hook.
// Hook yang comprehensive — data, loading, error, dan actions
const useLocalStorage = <T>(key: string, initialValue: T) => {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setValue = useCallback((value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.error(error)
}
}, [key, storedValue])
const removeValue = useCallback(() => {
localStorage.removeItem(key)
setStoredValue(initialValue)
}, [key, initialValue])
return [storedValue, setValue, removeValue] as const
}
5.2 Factory Hook Pattern
Hook yang menerima konfigurasi dan mengembalikan hook yang sudah di-konfigurasi.
// Factory — buat hook dengan konfigurasi berbeda
const createApiHook = <T>(endpoint: string, defaultParams?: Params) => {
return (params?: Params) => {
return useQuery({
queryKey: [endpoint, { ...defaultParams, ...params }],
queryFn: () => api.get<T>(endpoint, { ...defaultParams, ...params }),
})
}
}
// Generate hook yang sudah terkonfigurasi
const useProducts = createApiHook<Product[]>('/products', { limit: 20 })
const useUsers = createApiHook<User[]>('/users')
const useOrders = createApiHook<Order[]>('/orders', { status: 'active' })
// Penggunaan sederhana
const ProductList = () => {
const { data } = useProducts({ category: 'electronics' })
// ...
}
5.3 Reducer Hook Pattern
Gunakan useReducer untuk state kompleks dengan banyak transisi.
type WizardState = {
step: number
data: Record<string, unknown>
errors: Record<string, string>
isSubmitting: boolean
}
type WizardAction =
| { type: 'NEXT' }
| { type: 'PREV' }
| { type: 'UPDATE_FIELD'; field: string; value: unknown }
| { type: 'SET_ERRORS'; errors: Record<string, string> }
| { type: 'SUBMIT_START' }
| { type: 'SUBMIT_SUCCESS' }
| { type: 'SUBMIT_ERROR'; error: string }
const wizardReducer = (state: WizardState, action: WizardAction): WizardState => {
switch (action.type) {
case 'NEXT': return { ...state, step: state.step + 1, errors: {} }
case 'PREV': return { ...state, step: state.step - 1 }
case 'UPDATE_FIELD':
return { ...state, data: { ...state.data, [action.field]: action.value } }
// ...
}
}
const useWizard = (steps: Step[]) => {
const [state, dispatch] = useReducer(wizardReducer, {
step: 0,
data: {},
errors: {},
isSubmitting: false,
})
return {
...state,
currentStep: steps[state.step],
isFirst: state.step === 0,
isLast: state.step === steps.length - 1,
next: () => dispatch({ type: 'NEXT' }),
prev: () => dispatch({ type: 'PREV' }),
updateField: (field: string, value: unknown) =>
dispatch({ type: 'UPDATE_FIELD', field, value }),
}
}
5.4 Event Emitter Hook Pattern
Komunikasi antar komponen yang tidak memiliki hubungan parent-child, tanpa global store.
type Events = {
'toast:show': { message: string; type: 'success' | 'error' }
'modal:open': { id: string }
'modal:close': void
}
const eventBus = mitt<Events>()
const useEventBus = () => {
const emit = useCallback(<K extends keyof Events>(
event: K,
payload: Events[K]
) => {
eventBus.emit(event, payload)
}, [])
const on = useCallback(<K extends keyof Events>(
event: K,
handler: (payload: Events[K]) => void
) => {
eventBus.on(event, handler)
return () => eventBus.off(event, handler) // cleanup
}, [])
return { emit, on }
}
// Di komponen manapun
const SomeButton = () => {
const { emit } = useEventBus()
return (
<button onClick={() => emit('toast:show', { message: 'Saved!', type: 'success' })}>
Save
</button>
)
}
const ToastContainer = () => {
const { on } = useEventBus()
const [toasts, setToasts] = useState<Toast[]>([])
useEffect(() => {
return on('toast:show', (payload) => {
setToasts(prev => [...prev, { ...payload, id: nanoid() }])
})
}, [on])
return <div>{toasts.map(t => <Toast key={t.id} {...t} />)}</div>
}
6. Rendering Patterns
Pattern yang mengontrol bagaimana dan kapan komponen di-render.

6.1 Conditional Rendering Patterns
// Guard clause — lebih bersih dari nested ternary
const UserProfile = ({ user }: { user: User | null }) => {
if (!user) return <EmptyState />
if (!user.isActive) return <DeactivatedState />
if (user.needsOnboarding) return <OnboardingFlow />
return <ProfileContent user={user} />
}
// Short-circuit untuk opsional element
const Card = ({ badge, title }: CardProps) => (
<div>
{badge && <Badge>{badge}</Badge>}
<h3>{title}</h3>
</div>
)
// Lookup object untuk banyak kondisi — lebih baik dari switch di JSX
const STATUS_COMPONENT = {
loading: <Spinner />,
error: <ErrorIcon />,
success: <CheckIcon />,
idle: null,
} as const
const StatusIndicator = ({ status }: { status: keyof typeof STATUS_COMPONENT }) => (
<div>{STATUS_COMPONENT[status]}</div>
)
6.2 List Rendering Pattern
// Selalu key yang stabil dan unik
const ProductList = ({ products }: { products: Product[] }) => (
<ul>
{products.map(product => (
// Gunakan ID dari data, bukan index
<ProductItem key={product.id} product={product} />
))}
</ul>
)
// Virtualisasi untuk list yang sangat panjang
import { FixedSizeList } from 'react-window'
const VirtualList = ({ items }: { items: Item[] }) => (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={80}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<ItemRow item={items[index]} />
</div>
)}
</FixedSizeList>
)
6.3 Suspense Pattern
Deklaratif loading state menggunakan React Suspense.
// Komponen yang "suspend" saat data belum siap
const UserProfile = ({ userId }: { userId: string }) => {
// useSuspenseQuery akan throw Promise jika data belum ada
const { data: user } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => api.getUser(userId),
})
return <ProfileCard user={user} />
}
// Parent menentukan loading dan error UI
const App = () => (
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile userId="123" />
</Suspense>
</ErrorBoundary>
)
6.4 Lazy Loading Pattern
Code splitting untuk komponen yang tidak langsung dibutuhkan.
import { lazy, Suspense } from 'react'
// Hanya di-download saat dibutuhkan
const HeavyChart = lazy(() => import('./HeavyChart'))
const AdminPanel = lazy(() => import('./AdminPanel'))
const ReportGenerator = lazy(() => import('./ReportGenerator'))
const Dashboard = () => {
const { isAdmin } = useAuth()
return (
<div>
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
{isAdmin && (
<Suspense fallback={<PanelSkeleton />}>
<AdminPanel />
</Suspense>
)}
</div>
)
}
// Route-based code splitting
const router = createBrowserRouter([
{ path: '/', element: <HomePage /> },
{
path: '/dashboard',
element: (
<Suspense fallback={<PageSkeleton />}>
{lazy(() => import('./pages/Dashboard'))}
</Suspense>
)
}
])
7. Performance Patterns
Pattern untuk optimasi performa rendering dan komputasi.
7.1 Memoization Pattern
Hindari re-render dan re-komputasi yang tidak perlu.
// React.memo — skip re-render jika props tidak berubah
const ExpensiveRow = memo(({ item, onDelete }: RowProps) => {
console.log('Render row:', item.id)
return (
<div>
<span>{item.name}</span>
<button onClick={() => onDelete(item.id)}>Delete</button>
</div>
)
})
// useMemo — cache hasil kalkulasi mahal
const ProductStats = ({ products }: { products: Product[] }) => {
const stats = useMemo(() => ({
total: products.length,
totalValue: products.reduce((sum, p) => sum + p.price, 0),
avgPrice: products.reduce((sum, p) => sum + p.price, 0) / products.length,
categories: [...new Set(products.map(p => p.category))],
}), [products])
return <StatsDisplay stats={stats} />
}
// useCallback — stable reference untuk handler
const ProductList = ({ products }: { products: Product[] }) => {
const [selected, setSelected] = useState<string[]>([])
// Tanpa useCallback, ini dibuat ulang tiap render
// → ExpensiveRow selalu re-render meskipun row tidak berubah
const handleDelete = useCallback((id: string) => {
setSelected(prev => prev.filter(s => s !== id))
}, []) // dependency array kosong = referensi stabil
return products.map(p => (
<ExpensiveRow key={p.id} item={p} onDelete={handleDelete} />
))
}
7.2 Debounce & Throttle Pattern
Batasi frekuensi eksekusi fungsi untuk operasi yang mahal.
// Debounce — tunda eksekusi sampai user berhenti mengetik
const useDebounce = <T>(value: T, delay: number): T => {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debounced
}
const SearchInput = () => {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
const { data } = useQuery({
queryKey: ['search', debouncedQuery],
queryFn: () => api.search(debouncedQuery),
enabled: debouncedQuery.length > 2,
})
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<SearchResults results={data} />
</div>
)
}
7.3 Code Splitting Pattern
Lihat 6.4 Lazy Loading Pattern. Tambahan — split berdasarkan route dan fitur besar, bukan komponen kecil.
7.4 State Colocation Pattern
Taruh state sedekat mungkin dengan komponen yang menggunakannya untuk membatasi scope re-render.
// BURUK — state di root menyebabkan seluruh tree re-render
const App = () => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false) // ← terlalu tinggi!
return (
<div>
<HeavyComponent1 />
<HeavyComponent2 />
<Dropdown isOpen={isDropdownOpen} onToggle={setIsDropdownOpen} />
</div>
)
}
// BAIK — state di dalam komponen yang membutuhkannya
const App = () => (
<div>
<HeavyComponent1 /> {/* tidak re-render saat dropdown toggle */}
<HeavyComponent2 /> {/* tidak re-render saat dropdown toggle */}
<Dropdown /> {/* manages its own open state */}
</div>
)
const Dropdown = () => {
const [isOpen, setIsOpen] = useState(false) // ← hanya ini yang re-render
return <div>{/* ... */}</div>
}
7.5 Window / Virtualization Pattern
Untuk list dengan ribuan item — hanya render yang terlihat di viewport.
import { useVirtualizer } from '@tanstack/react-virtual'
const VirtualTable = ({ rows }: { rows: Row[] }) => {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48,
overscan: 5,
})
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(virtualItem => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: virtualItem.start,
height: virtualItem.size,
width: '100%',
}}
>
<TableRow row={rows[virtualItem.index]} />
</div>
))}
</div>
</div>
)
}
8. Architecture & Structure Patterns
Pattern untuk mengorganisir kode dalam skala besar.
8.1 Feature-Based Folder Structure
Organisasi kode berdasarkan fitur/domain, bukan berdasarkan tipe file.
src/
├── features/
│ ├── auth/
│ │ ├── components/ # LoginForm, RegisterForm
│ │ ├── hooks/ # useAuth, useLogin
│ │ ├── services/ # authService, tokenService
│ │ ├── store/ # authStore (Zustand)
│ │ ├── types/ # User, Credentials
│ │ └── index.ts # public API dari feature ini
│ ├── products/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ └── index.ts
│ └── orders/
│ └── ...
├── shared/
│ ├── components/ # Button, Input, Modal (atom/molekul)
│ ├── hooks/ # useDebounce, useLocalStorage
│ ├── utils/ # formatDate, formatCurrency
│ └── types/ # shared types
├── pages/ # route components — thin, compose features
└── app/ # providers, router, global config
8.2 Barrel Export Pattern
Export publik API dari module melalui file index.ts.
// features/auth/index.ts — hanya export yang perlu diketahui consumer
export { LoginForm } from './components/LoginForm'
export { RegisterForm } from './components/RegisterForm'
export { useAuth } from './hooks/useAuth'
export { AuthProvider } from './providers/AuthProvider'
export type { User, AuthState } from './types'
// Internal components/logic tidak di-export
// Konsumer tidak perlu tahu struktur internal feature
// Penggunaan di tempat lain
import { LoginForm, useAuth } from '@/features/auth'
8.3 Separation of Concerns (Layer Architecture)
Pisahkan layer: UI Layer → State Layer → Business Logic Layer → Data Layer.
// Data Layer — hanya komunikasi dengan API
const userApi = {
getUser: (id: string) => fetch(`/api/users/${id}`).then(r => r.json()),
updateUser: (id: string, data: Partial<User>) =>
fetch(`/api/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }).then(r => r.json()),
}
// Business Logic Layer — transformasi dan aturan bisnis
const userService = {
getDisplayName: (user: User) => `${user.firstName} ${user.lastName}`,
canEditProfile: (viewer: User, target: User) =>
viewer.id === target.id || viewer.role === 'admin',
formatUserForApi: (formData: UserForm): Partial<User> => ({
firstName: formData.name.split(' ')[0],
lastName: formData.name.split(' ').slice(1).join(' '),
isActive: formData.status === 'active',
}),
}
// State Layer — hook yang koordinasi API + business logic + state
const useUserProfile = (userId: string) => {
const query = useQuery({
queryKey: ['user', userId],
queryFn: () => userApi.getUser(userId),
})
const mutation = useMutation({
mutationFn: (data: UserForm) =>
userApi.updateUser(userId, userService.formatUserForApi(data)),
})
return {
user: query.data,
isLoading: query.isLoading,
displayName: query.data ? userService.getDisplayName(query.data) : '',
updateUser: mutation.mutate,
isUpdating: mutation.isPending,
}
}
// UI Layer — hanya render, zero business logic
const UserProfilePage = ({ userId }: { userId: string }) => {
const { user, displayName, isLoading, updateUser } = useUserProfile(userId)
if (isLoading) return <Skeleton />
return (
<div>
<h1>{displayName}</h1>
<UserForm initialData={user} onSubmit={updateUser} />
</div>
)
}
8.4 MVVM Pattern (Model-View-ViewModel)
Adaptasi MVVM untuk React — ViewModel adalah custom hook yang menjadi jembatan antara Model (data/service) dan View (komponen).
// Model — data dan operasi
const productService = { getProducts, createProduct, deleteProduct }
// ViewModel — hook yang expose state dan action untuk View
const useProductsViewModel = () => {
const [search, setSearch] = useState('')
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const { data: allProducts = [] } = useQuery({
queryKey: ['products'],
queryFn: productService.getProducts,
})
const filteredProducts = useMemo(() =>
allProducts
.filter(p => p.name.toLowerCase().includes(search.toLowerCase()))
.filter(p => !selectedCategory || p.category === selectedCategory),
[allProducts, search, selectedCategory]
)
const categories = useMemo(() =>
[...new Set(allProducts.map(p => p.category))],
[allProducts]
)
return {
// State
search, selectedCategory,
products: filteredProducts,
categories,
// Actions
setSearch, setSelectedCategory,
}
}
// View — hanya render, gunakan ViewModel
const ProductsView = () => {
const vm = useProductsViewModel()
return (
<div>
<SearchBar value={vm.search} onChange={vm.setSearch} />
<CategoryFilter
categories={vm.categories}
selected={vm.selectedCategory}
onSelect={vm.setSelectedCategory}
/>
<ProductGrid products={vm.products} />
</div>
)
}
9. Form Patterns
Pattern khusus untuk mengelola form yang kompleks.
9.1 Controlled Form Pattern
Semua form state dikontrol oleh React state.
const useFormField = <T>(initialValue: T, validate?: (v: T) => string | undefined) => {
const [value, setValue] = useState(initialValue)
const [touched, setTouched] = useState(false)
const error = touched ? validate?.(value) : undefined
return {
value,
onChange: (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value as T),
onBlur: () => setTouched(true),
error,
isValid: !error,
}
}
const LoginForm = ({ onSubmit }: { onSubmit: (data: LoginData) => void }) => {
const email = useFormField('', v => !v.includes('@') ? 'Invalid email' : undefined)
const password = useFormField('', v => v.length < 8 ? 'Too short' : undefined)
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
if (email.isValid && password.isValid) {
onSubmit({ email: email.value, password: password.value })
}
}
return (
<form onSubmit={handleSubmit}>
<Field label="Email" error={email.error}>
<input type="email" {...email} />
</Field>
<Field label="Password" error={password.error}>
<input type="password" {...password} />
</Field>
<button type="submit">Login</button>
</form>
)
}
9.2 React Hook Form Pattern
Gunakan library untuk performa form yang optimal (uncontrolled + register pattern).
import { useForm, Controller } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const schema = z.object({
name: z.string().min(2, 'Too short'),
email: z.string().email('Invalid email'),
role: z.enum(['admin', 'user', 'viewer']),
bio: z.string().max(500).optional(),
})
type FormData = z.infer<typeof schema>
const UserForm = ({ onSubmit }: { onSubmit: (data: FormData) => void }) => {
const { register, handleSubmit, control, formState: { errors, isSubmitting } } =
useForm<FormData>({ resolver: zodResolver(schema) })
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} placeholder="Name" />
{errors.name && <span>{errors.name.message}</span>}
<Controller
name="role"
control={control}
render={({ field }) => (
<Select {...field} options={roleOptions} />
)}
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save'}
</button>
</form>
)
}
9.3 Multi-Step Form Pattern
Wizard form dengan state yang shared antar step.
const useMultiStepForm = <T extends object>(steps: Step[], initialData: T) => {
const [currentStep, setCurrentStep] = useState(0)
const [formData, setFormData] = useState<T>(initialData)
const updateData = (newData: Partial<T>) => {
setFormData(prev => ({ ...prev, ...newData }))
}
return {
step: steps[currentStep],
stepIndex: currentStep,
totalSteps: steps.length,
isFirst: currentStep === 0,
isLast: currentStep === steps.length - 1,
formData,
updateData,
next: () => setCurrentStep(s => Math.min(s + 1, steps.length - 1)),
prev: () => setCurrentStep(s => Math.max(s - 1, 0)),
goTo: setCurrentStep,
}
}
10. Testing Patterns
Pattern untuk menulis test yang maintainable dan efektif.
10.1 Testing Library Pattern
Test behaviour, bukan implementation.
// Buruk — test implementation detail
expect(wrapper.state('isOpen')).toBe(true)
expect(component.find('.dropdown-menu').exists()).toBe(true)
// Baik — test dari perspektif user
test('opens dropdown when button is clicked', async () => {
const user = userEvent.setup()
render(<Dropdown options={['A', 'B', 'C']} />)
const trigger = screen.getByRole('button', { name: /select option/i })
await user.click(trigger)
expect(screen.getByRole('listbox')).toBeVisible()
expect(screen.getAllByRole('option')).toHaveLength(3)
})
10.2 Custom Hook Testing Pattern
Test hooks secara isolated menggunakan renderHook.
import { renderHook, act } from '@testing-library/react'
test('useCounter increments and decrements', () => {
const { result } = renderHook(() => useCounter(0))
act(() => result.current.increment())
expect(result.current.count).toBe(1)
act(() => result.current.decrement())
expect(result.current.count).toBe(0)
})
// Wrap dengan provider jika hook butuh context
test('useAuth returns user', () => {
const wrapper = ({ children }: { children: ReactNode }) => (
<AuthProvider initialUser={mockUser}>{children}</AuthProvider>
)
const { result } = renderHook(() => useAuth(), { wrapper })
expect(result.current.user).toEqual(mockUser)
})
10.3 MSW (Mock Service Worker) Pattern
Intercept HTTP request di test untuk realistic integration testing.
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer(
http.get('/api/users', () => {
return HttpResponse.json([
{ id: '1', name: 'Alice', role: 'admin' },
{ id: '2', name: 'Bob', role: 'user' },
])
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json()
return HttpResponse.json({ id: '3', ...body }, { status: 201 })
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
test('renders user list from API', async () => {
render(<UserList />)
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument()
expect(screen.getByText('Bob')).toBeInTheDocument()
})
})
11. Accessibility Patterns
Pattern untuk memastikan komponen accessible.
11.1 ARIA Pattern
Sematkan ARIA attributes yang tepat untuk screen readers.
// Dialog yang accessible
const Dialog = ({ isOpen, onClose, title, children }: DialogProps) => {
const titleId = useId()
const descId = useId()
const dialogRef = useRef<HTMLDivElement>(null)
// Trap focus di dalam dialog
useFocusTrap(dialogRef, isOpen)
// Close dengan Escape
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
if (isOpen) document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [isOpen, onClose])
if (!isOpen) return null
return createPortal(
<div role="dialog" aria-modal aria-labelledby={titleId} ref={dialogRef}>
<h2 id={titleId}>{title}</h2>
<div id={descId}>{children}</div>
<button onClick={onClose} aria-label="Close dialog">✕</button>
</div>,
document.body
)
}
11.2 Skip Navigation Pattern
Izinkan keyboard user untuk skip ke konten utama.
const SkipToContent = () => (
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50"
>
Skip to main content
</a>
)
const Layout = ({ children }: { children: ReactNode }) => (
<>
<SkipToContent />
<Header />
<main id="main-content" tabIndex={-1}>
{children}
</main>
<Footer />
</>
)
12. React 18 / 19 Modern Patterns
Pattern terbaru yang memanfaatkan fitur React 18 dan 19.

12.1 Server Components Pattern (RSC)
Komponen yang di-render di server, mengurangi JavaScript yang dikirim ke client.
// app/users/page.tsx — Server Component (default di Next.js App Router)
// Tidak ada 'use client' — ini server component
const UsersPage = async () => {
// Fetch langsung di component, tanpa useEffect atau useState
const users = await db.user.findMany({ where: { isActive: true } })
return (
<div>
<h1>Users</h1>
{/* Hanya komponen yang butuh interaktivitas yang perlu 'use client' */}
<UserSearch />
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
// components/UserSearch.tsx — Client Component untuk interaktivitas
'use client'
const UserSearch = () => {
const [query, setQuery] = useState('')
return <input value={query} onChange={e => setQuery(e.target.value)} />
}
12.2 Server Actions Pattern
Mutasi data langsung dari komponen tanpa perlu API route.
// actions/user.ts
'use server'
export const updateUser = async (id: string, formData: FormData) => {
const name = formData.get('name') as string
await db.user.update({ where: { id }, data: { name } })
revalidatePath('/users')
}
// Komponen
const EditUserForm = ({ user }: { user: User }) => (
<form action={updateUser.bind(null, user.id)}>
<input name="name" defaultValue={user.name} />
<button type="submit">Save</button>
</form>
)
12.3 useTransition Pattern
Tandai update state sebagai non-urgent untuk tetap responsif saat expensive render.
const SearchPage = () => {
const [query, setQuery] = useState('')
const [results, setResults] = useState<Result[]>([])
const [isPending, startTransition] = useTransition()
const handleSearch = (value: string) => {
setQuery(value) // Urgent — update input segera
startTransition(() => {
// Non-urgent — bisa diinterrupt jika user terus mengetik
setResults(expensiveSearch(value))
})
}
return (
<div>
<input value={query} onChange={e => handleSearch(e.target.value)} />
{isPending ? <Spinner /> : <ResultsList results={results} />}
</div>
)
}
12.4 useDeferredValue Pattern
Versi "deferred" dari value — UI tetap responsif meski komputasi lambat.
const ProductSearch = ({ products }: { products: Product[] }) => {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
// Ini pakai deferredQuery — render dengan value lama dulu
// sambil menunggu render dengan value baru selesai
const filtered = useMemo(
() => products.filter(p => p.name.includes(deferredQuery)),
[products, deferredQuery]
)
const isStale = query !== deferredQuery
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.5 : 1 }}>
{filtered.map(p => <ProductCard key={p.id} product={p} />)}
</div>
</div>
)
}
12.5 use() Hook Pattern (React 19)
Baca resource (Promise atau Context) di dalam render function.
// React 19
const UserProfile = ({ userPromise }: { userPromise: Promise<User> }) => {
// use() bisa dipanggil secara conditional (berbeda dari hooks biasa)
const user = use(userPromise) // suspend otomatis saat Promise belum resolve
return <ProfileCard user={user} />
}
// Di parent
const Page = ({ userId }: { userId: string }) => {
const userPromise = fetchUser(userId) // tidak di-await
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
)
}
12.6 Concurrent Features Pattern
Manfaatkan React 18 Concurrent Mode untuk rendering yang lebih smooth.
// startTransition — untuk navigasi dan tab switching
const TabSwitcher = ({ tabs }: { tabs: Tab[] }) => {
const [activeTab, setActiveTab] = useState(tabs[0].id)
const [isPending, startTransition] = useTransition()
return (
<div>
<TabList>
{tabs.map(tab => (
<TabButton
key={tab.id}
isActive={activeTab === tab.id}
isPending={isPending}
onClick={() => {
startTransition(() => setActiveTab(tab.id))
}}
>
{tab.label}
</TabButton>
))}
</TabList>
<TabPanel tabId={activeTab} />
</div>
)
}
13. Prinsip & Filosofi
Prinsip-prinsip yang mendasari semua keputusan arsitektur React.
13.1 Single Responsibility Principle (SRP)
Satu komponen = satu tanggung jawab yang jelas.
❌ UserDashboardFetchAndDisplayAndEditAndDeleteComponent
✅ UserDashboardContainer (fetch + orchestrate)
✅ UserProfileCard (display)
✅ UserEditModal (edit form)
13.2 DRY (Don't Repeat Yourself)
Ekstrak logika yang duplikat ke custom hook atau utility — tapi jangan terlalu dini. Duplikasi lebih baik dari abstraksi yang salah.
"Duplication is far cheaper than the wrong abstraction." — Sandi Metz
13.3 YAGNI (You Aren't Gonna Need It)
Jangan build fitur yang "mungkin akan dibutuhkan nanti." Build yang dibutuhkan sekarang dengan design yang bisa di-extend nanti.
13.4 KISS (Keep It Simple, Stupid)
Solusi sederhana hampir selalu lebih baik. Kalau butuh diagram panjang untuk jelaskan kode, kode itu terlalu kompleks.
13.5 Stable Dependency Principle
Bergantung pada hal yang stabil, bukan yang volatile. Jangan import private API dari library (yang diawali _). Jangan gunakan CSS class name sebagai selector test.
13.6 Composition over Inheritance
React by nature menggunakan komposisi. Extend behavior dengan compose komponen, bukan dengan class inheritance.
// Inheritance (hindari di React)
class SpecialButton extends Button { ... }
// Composition (embrace)
const SpecialButton = (props: ButtonProps) => (
<Button {...props} className={clsx('special', props.className)}>
<StarIcon />
{props.children}
</Button>
)
14. Anti-Patterns (Yang Harus Dihindari)
Pattern yang sering ditulis tapi seharusnya dihindari.
❌ Prop Drilling Berlebihan
Passing props melalui 5+ level komponen. Solusi: Context, state manager, atau component composition.
❌ useEffect untuk Derived State
// BURUK
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [fullName, setFullName] = useState('') // ← tidak perlu!
useEffect(() => {
setFullName(`${firstName} ${lastName}`)
}, [firstName, lastName])
// BAIK — compute langsung
const fullName = `${firstName} ${lastName}`
❌ useEffect untuk Event Handler
// BURUK — useEffect tidak diperlukan di sini
useEffect(() => {
if (isSubmitting) {
submitForm()
}
}, [isSubmitting])
// BAIK — panggil langsung di handler
const handleSubmit = () => {
setIsSubmitting(true)
submitForm()
}
❌ Index sebagai Key di List
// BURUK — key tidak stabil jika list bisa berubah urutan
{items.map((item, index) => <Item key={index} item={item} />)}
// BAIK — gunakan ID yang stabil dari data
{items.map(item => <Item key={item.id} item={item} />)}
❌ Premature Optimization
Menambahkan useMemo dan useCallback ke semua tempat sebelum ada bukti performa bermasalah. Kedua hook ini punya overhead tersendiri.
// TIDAK PERLU untuk operasi murah
const value = useMemo(() => a + b, [a, b]) // overhead > manfaat
// PERLU untuk kalkulasi mahal atau reference stability
const sorted = useMemo(() => [...bigArray].sort(compareFn), [bigArray])
❌ Giant Context yang Sering Berubah
// BURUK — semua consumer re-render tiap ada perubahan apapun
const AppContext = createContext({
user, theme, notifications, cart, searchQuery, ...
})
// BAIK — split ke context yang focused
const UserContext = createContext(user)
const ThemeContext = createContext(theme)
const CartContext = createContext(cart)
❌ Copying Props ke State
// BURUK — state jadi out-of-sync dengan prop
const Input = ({ value: propValue }: InputProps) => {
const [value, setValue] = useState(propValue) // ← tidak sync jika prop berubah!
...
}
// BAIK — controlled component
const Input = ({ value, onChange }: InputProps) => (
<input value={value} onChange={onChange} />
)
❌ Nested Component Definition
// BURUK — InnerComponent di-recreate tiap render → unmount/remount terus
const Outer = () => {
const Inner = () => <div>Hello</div> // ← definisi di dalam render!
return <Inner />
}
// BAIK — definisikan di luar
const Inner = () => <div>Hello</div>
const Outer = () => <Inner />
❌ Clean Architecture Penuh di React
Terlalu banyak lapisan abstraksi yang tidak natural untuk ekosistem React akan membuat kode sulit dibaca dan tidak idiomatic. React sudah punya pattern-nya sendiri maka gunakan itu.
Ringkasan — Panduan Cepat Memilih Pattern
| Situasi | Pattern yang Direkomendasikan |
|---|---|
| Logika reusable antar komponen | Custom Hook |
| UI tanpa logika, data dari parent | Presentational Component |
| Komponen UI kompleks yang customizable | Compound Components |
| State dibutuhkan banyak komponen | Provider / Zustand |
| Komponen yang bisa di-style bebas | Headless Component |
| Render berbeda berdasarkan kondisi | Conditional Rendering |
| List panjang (1000+ item) | Virtualization |
| Fetch data dari API | TanStack Query + Custom Hook |
| Form kompleks | React Hook Form + Zod |
| Async loading state | Suspense + Error Boundary |
| Performa re-render lambat | memo + useMemo + useCallback |
| UI yang perlu escape container CSS | Portal |
| Routing + state yang shareable | URL State |
| App besar multi-team | Feature-based folder + Barrel export |
Dokumen ini mencakup pattern dari React 16 hingga React 19, termasuk ekosistem modern (TanStack Query, Zustand, React Hook Form, Next.js App Router). Pattern terus berkembang seiring evolusi React.