Component Design Patterns
Table of Contents + −
React components can be structured in various ways to solve different problems. This guide covers common component design patterns that help create maintainable and reusable code.
Container/Presentational Pattern
This pattern separates components into two categories:
- Container Components: Handle data fetching, state management, and business logic
- Presentational Components: Focus on rendering UI based on props
// Container Componentfunction UserListContainer() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true);
useEffect(() => { fetchUsers().then((data) => { setUsers(data); setLoading(false); }); }, []);
return <UserList users={users} loading={loading} />;}
// Presentational Componentfunction UserList({ users, loading }) { if (loading) return <div>Loading...</div>;
return ( <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> );}
Higher-Order Components (HOC)
HOCs are functions that take a component and return a new component with additional props or behavior.
// HOC that adds loading statefunction withLoading(WrappedComponent) { return function WithLoadingComponent({ isLoading, ...props }) { if (isLoading) return <div>Loading...</div>; return <WrappedComponent {...props} />; };}
// Usageconst UserListWithLoading = withLoading(UserList);
Render Props
Render props involve passing a function as a prop that returns React elements.
function DataFetcher({ url, render }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true);
useEffect(() => { fetch(url) .then((res) => res.json()) .then((data) => { setData(data); setLoading(false); }); }, [url]);
return render({ data, loading });}
// Usage<DataFetcher url="/api/users" render={({ data, loading }) => loading ? <div>Loading...</div> : <UserList users={data} /> }/>;
Custom Hooks
Custom hooks allow you to extract component logic into reusable functions.
function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { fetch(url) .then((res) => res.json()) .then((data) => { setData(data); setLoading(false); }) .catch((err) => { setError(err); setLoading(false); }); }, [url]);
return { data, loading, error };}
// Usagefunction UserList() { const { data, loading, error } = useFetch("/api/users");
if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>;
return ( <ul> {data.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> );}
Compound Components
Compound components work together to form a cohesive UI element.
function Select({ children, value, onChange }) { return ( <div className="select"> <select value={value} onChange={(e) => onChange(e.target.value)}> {children} </select> </div> );}
Select.Option = function Option({ value, children }) { return <option value={value}>{children}</option>;};
// Usage<Select value={selected} onChange={setSelected}> <Select.Option value="apple">Apple</Select.Option> <Select.Option value="banana">Banana</Select.Option> <Select.Option value="cherry">Cherry</Select.Option></Select>;
Best Practices
- Keep components focused on a single responsibility
- Use composition over inheritance
- Make components reusable through props
- Extract complex logic into custom hooks
- Use TypeScript for better type safety
- Document component props and usage
Conclusion
Understanding these patterns will help you write more maintainable and scalable React applications. Choose the pattern that best fits your specific use case.