Component Design Patterns

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:

  1. Container Components: Handle data fetching, state management, and business logic
  2. Presentational Components: Focus on rendering UI based on props
// Container Component
function 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 Component
function 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 state
function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) return <div>Loading...</div>;
return <WrappedComponent {...props} />;
};
}
// Usage
const 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 };
}
// Usage
function 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

  1. Keep components focused on a single responsibility
  2. Use composition over inheritance
  3. Make components reusable through props
  4. Extract complex logic into custom hooks
  5. Use TypeScript for better type safety
  6. 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.

Share & Connect