Advanced React Patterns
As your React applications grow in complexity, you’ll need to employ advanced patterns to maintain clean, scalable, and performant code. This guide explores some of the most powerful patterns available in modern React development.
Compound Components Pattern
The compound components pattern allows you to create components that work together to form a complete UI element:
function Accordion() {
return (
<Accordion.Root>
<Accordion.Item value="item1">
<Accordion.Header>
<Accordion.Trigger>What is React?</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
React is a JavaScript library for building user interfaces.
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
}
This pattern provides excellent flexibility while maintaining a clean API.
Render Props Pattern
Render props allow you to share code between components using a prop whose value is a function:
function DataFetcher({ render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData().then(result => {
setData(result);
setLoading(false);
});
}, []);
return render({ data, loading });
}
// Usage
<DataFetcher
render={({ data, loading }) =>
loading ? <Spinner /> : <DataDisplay data={data} />
}
/>
Higher-Order Components (HOCs)
HOCs are functions that take a component and return a new component with additional functionality:
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { user } = useAuth();
if (!user) {
return <LoginForm />;
}
return <WrappedComponent {...props} user={user} />;
};
}
// Usage
const ProtectedDashboard = withAuth(Dashboard);
Custom Hooks Pattern
Custom hooks let you extract component logic into reusable functions:
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = value => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
);
}
Context + Reducer Pattern
Combine Context API with useReducer for complex state management:
const AppContext = createContext();
function appReducer(state, action) {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_THEME':
return { ...state, theme: action.payload };
default:
return state;
}
}
function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, {
user: null,
theme: 'light'
});
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}
function useAppContext() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useAppContext must be used within AppProvider');
}
return context;
}
Performance Patterns
Memoization
Use React.memo, useMemo, and useCallback strategically:
const ExpensiveComponent = React.memo(({ data, onItemClick }) => {
const processedData = useMemo(() => {
return data.map(item => ({
...item,
processed: expensiveProcessing(item)
}));
}, [data]);
const handleClick = useCallback((id) => {
onItemClick(id);
}, [onItemClick]);
return (
<div>
{processedData.map(item => (
<Item
key={item.id}
data={item}
onClick={handleClick}
/>
))}
</div>
);
});
Code Splitting
Split your code to improve initial load times:
import { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
Error Boundaries
Handle errors gracefully in your React tree:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<App />
</ErrorBoundary>
Conclusion
These advanced patterns will help you build more maintainable, performant, and scalable React applications. Remember to choose the right pattern for your specific use case and always prioritize code readability and maintainability.
