React Component Patterns I Use Every Day
After years of React, these are the patterns that have stood the test of time. Simple, effective, and maintainable.
Patterns That Actually Help
There are hundreds of React patterns out there. Most are overengineered. Here are the ones I actually use.
Composition Over Props
Instead of prop drilling or complex configurations:
// ❌ Prop-heavy
<Card
title="Hello"
subtitle="World"
showFooter
footerContent="Footer text"
variant="primary"
/>
// ✅ Composition
<Card>
<Card.Header>
<Card.Title>Hello</Card.Title>
<Card.Subtitle>World</Card.Subtitle>
</Card.Header>
<Card.Footer>Footer text</Card.Footer>
</Card>
More flexible, easier to extend.
Custom Hooks for Logic
Extract stateful logic into hooks:
// ❌ Logic in component
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
// ... render
}
// ✅ Custom hook
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
return { user, loading, error };
}
function UserProfile({ userId }) {
const { user, loading, error } = useUser(userId);
// ... render
}
The hook is reusable and testable.
Controlled Components
Keep form state in the parent:
function Form() {
const [values, setValues] = useState({ name: '', email: '' });
const handleChange = (e) => {
setValues(prev => ({
...prev,
[e.target.name]: e.target.value
}));
};
return (
<form>
<Input
name="name"
value={values.name}
onChange={handleChange}
/>
<Input
name="email"
value={values.email}
onChange={handleChange}
/>
</form>
);
}
Predictable state, easy debugging.
Early Returns
Handle edge cases first:
function UserCard({ user }) {
if (!user) return null;
if (user.isLoading) return <Spinner />;
if (user.error) return <Error message={user.error} />;
return (
<div className="card">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Clear, readable, no nested conditionals.
Container/Presentational
Separate data fetching from rendering:
// Container - handles data
function UserListContainer() {
const { users, loading } = useUsers();
return <UserList users={users} loading={loading} />;
}
// Presentational - just renders
function UserList({ users, loading }) {
if (loading) return <Spinner />;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
Presentational components are easy to test and reuse.
Keep It Simple
The best pattern is the one that's easiest to understand six months later. Don't over-abstract.