React's flexibility is both a strength and a challenge. Without established patterns, codebases can become difficult to maintain. In this guide, we'll explore battle-tested patterns that help you build scalable, maintainable React applications that your team will love working with.
Compound Components Pattern
Compound components let you create expressive, flexible APIs. Think of HTML's select and option elements - they work together as a unit. This pattern is perfect for complex UI components like tabs, accordions, menus, and form fields.
typescript
// Compound Components Pattern
import { createContext, useContext, useState, ReactNode } from 'react';
interface TabsContextType {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextType | null>(null);
function useTabs() {
const context = useContext(TabsContext);
if (!context) throw new Error('useTabs must be used within Tabs');
return context;
}
interface TabsProps {
defaultTab: string;
children: ReactNode;
onChange?: (tab: string) => void;
}
function Tabs({ defaultTab, children, onChange }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultTab);
const handleTabChange = (tab: string) => {
setActiveTab(tab);
onChange?.(tab);
};
return (
<TabsContext.Provider value={{ activeTab, setActiveTab: handleTabChange }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }: { children: ReactNode }) {
return <div className="tab-list" role="tablist">{children}</div>;
}
function Tab({ id, children }: { id: string; children: ReactNode }) {
const { activeTab, setActiveTab } = useTabs();
return (
<button
role="tab"
aria-selected={activeTab === id}
className={`tab ${activeTab === id ? 'active' : ''}`}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
}
function TabPanels({ children }: { children: ReactNode }) {
return <div className="tab-panels">{children}</div>;
}
function TabPanel({ id, children }: { id: string; children: ReactNode }) {
const { activeTab } = useTabs();
if (activeTab !== id) return null;
return <div role="tabpanel" className="tab-panel">{children}</div>;
}
// Attach sub-components
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panels = TabPanels;
Tabs.Panel = TabPanel;
// Usage - Clean, readable API
<Tabs defaultTab="profile" onChange={console.log}>
<Tabs.List>
<Tabs.Tab id="profile">Profile</Tabs.Tab>
<Tabs.Tab id="settings">Settings</Tabs.Tab>
<Tabs.Tab id="billing">Billing</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel id="profile"><ProfileContent /></Tabs.Panel>
<Tabs.Panel id="settings"><SettingsContent /></Tabs.Panel>
<Tabs.Panel id="billing"><BillingContent /></Tabs.Panel>
</Tabs.Panels>
</Tabs>Custom Hooks Best Practices
Custom hooks are the primary way to share logic in React. A well-designed custom hook encapsulates complexity, provides a clean API, and is thoroughly tested. Follow the 'use' prefix convention and ensure hooks are composable.
typescript
// Custom Hook - useAsync with proper patterns
import { useState, useCallback, useRef, useEffect } from 'react';
interface AsyncState<T> {
data: T | null;
error: Error | null;
status: 'idle' | 'pending' | 'success' | 'error';
}
interface UseAsyncReturn<T> extends AsyncState<T> {
execute: (...args: unknown[]) => Promise<T>;
reset: () => void;
isLoading: boolean;
isError: boolean;
isSuccess: boolean;
}
function useAsync<T>(
asyncFunction: (...args: unknown[]) => Promise<T>,
immediate = false
): UseAsyncReturn<T> {
const [state, setState] = useState<AsyncState<T>>({
data: null,
error: null,
status: 'idle',
});
// Use ref to track mounted state
const mountedRef = useRef(true);
useEffect(() => {
return () => { mountedRef.current = false; };
}, []);
const execute = useCallback(async (...args: unknown[]) => {
setState({ data: null, error: null, status: 'pending' });
try {
const data = await asyncFunction(...args);
if (mountedRef.current) {
setState({ data, error: null, status: 'success' });
}
return data;
} catch (error) {
if (mountedRef.current) {
setState({ data: null, error: error as Error, status: 'error' });
}
throw error;
}
}, [asyncFunction]);
const reset = useCallback(() => {
setState({ data: null, error: null, status: 'idle' });
}, []);
useEffect(() => {
if (immediate) execute();
}, [immediate, execute]);
return {
...state,
execute,
reset,
isLoading: state.status === 'pending',
isError: state.status === 'error',
isSuccess: state.status === 'success',
};
}
// Usage
const { data, isLoading, isError, execute } = useAsync(fetchUsers);5+Patterns covered
50%Less prop drilling
Type-safeAll examples
ReusableCopy-paste ready