DEBI PRAHARADIKA
← Back to Blog Index
Frontend & UI Development2026-06-1320 min read

Referensi React Design Patterns

Daftar design pattern, prinsip arsitektur, dan best practice dalam pengembangan React modern, mencakup pattern yang umum dibahas, termasuk React 18/19 dan ekosistem modern.

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

  1. Core Component Patterns
  2. State Management Patterns
  3. Data Fetching Patterns
  4. Composition Patterns
  5. Hook Patterns
  6. Rendering Patterns
  7. Performance Patterns
  8. Architecture & Structure Patterns
  9. Form Patterns
  10. Testing Patterns
  11. Accessibility Patterns
  12. React 18 / 19 Modern Patterns
  13. Prinsip & Filosofi
  14. Anti-Patterns (Yang Harus Dihindari)

1. Core Component Patterns

Pattern dasar yang menjadi fondasi cara berpikir di React.

1.1 Component Composition Pattern

React Architecture Comparison: Monolith vs Composition

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

React Higher-Order Components vs Custom Hooks 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.

Modern React State Management Flow

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.

Modern Data Fetching and Caching Architecture

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.

React Rendering & Code Splitting Architecture

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.

SSR vs CSR Architecture Flow in Next.js / React

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.