React has evolved significantly since its inception, and with it, the patterns we use to build applications have matured. In this post, we'll explore modern React patterns that help create more maintainable, scalable, and performant applications.
1. Component Composition
Composition is one of React's core strengths. Instead of creating complex components that try to do too much, we can build small, focused components and compose them together.
// Bad: Monolithic component
function UserProfile(props) {
return (
<div className="profile">
<div className="header">
<img src={props.avatar} />
<h1>{props.name}</h1>
</div>
<div className="content">
{/* Lots of profile content */}
</div>
</div>
);
}
// Good: Composed components
function UserProfile(props) {
return (
<ProfileContainer>
<ProfileHeader avatar={props.avatar} name={props.name} />
<ProfileContent {...props} />
</ProfileContainer>
);
}
2. Custom Hooks for Logic Reuse
Custom hooks allow us to extract component logic into reusable functions. This pattern helps reduce duplication and keeps components focused on rendering.
// Custom hook for form handling
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value
});
};
const resetForm = () => {
setValues(initialValues);
};
return {
values,
handleChange,
resetForm
};
}
// Using the custom hook
function LoginForm() {
const { values, handleChange } = useForm({
email: '',
password: ''
});
return (
<form>
<input
name="email"
value={values.email}
onChange={handleChange}
/>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
/>
</form>
);
}
3. Compound Components
Compound components work together to form a complete UI. This pattern provides flexibility while maintaining an implicit shared state.
// Tabs component using compound pattern
function Tabs({ children }) {
const [activeIndex, setActiveIndex] = useState(0);
return React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
isActive: index === activeIndex,
onSelect: () => setActiveIndex(index)
});
});
}
function Tab({ isActive, onSelect, children }) {
return (
<button
onClick={onSelect}
style={{ fontWeight: isActive ? 'bold' : 'normal' }}
>
{children}
</button>
);
}
// Usage
<Tabs>
<Tab>First Tab</Tab>
<Tab>Second Tab</Tab>
<Tab>Third Tab</Tab>
</Tabs>
4. State Management with Context + useReducer
For medium-sized applications, combining Context with useReducer can provide a lightweight alternative to libraries like Redux.
// Create context
const AppContext = createContext();
// Reducer function
function appReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, action.payload]
};
case 'REMOVE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
default:
return state;
}
}
// Context provider component
function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, { todos: [] });
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}
// Custom hook to use context
function useAppContext() {
return useContext(AppContext);
}
// Usage in component
function TodoList() {
const { state, dispatch } = useAppContext();
return (
<div>
{state.todos.map(todo => (
<div key={todo.id}>
{todo.text}
<button onClick={() => dispatch({
type: 'REMOVE_TODO',
payload: todo.id
})}>
Remove
</button>
</div>
))}
</div>
);
}
5. Render Props Pattern
The render prop pattern allows components to share code by passing a function as a prop. This is powerful for behavior encapsulation.
// Mouse tracker with render prop
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e) => {
setPosition({
x: e.clientX,
y: e.clientY
});
};
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{render(position)}
</div>
);
}
// Usage
<MouseTracker render={({ x, y }) => (
<h1>
The mouse position is ({x}, {y})
</h1>
)} />
6. Higher-Order Components (HOCs)
HOCs are functions that take a component and return a new component with additional props or behavior.
// HOC for authentication
function withAuth(WrappedComponent) {
return function(props) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
// Check auth status
checkAuth().then(authStatus => {
setIsAuthenticated(authStatus);
});
}, []);
if (!isAuthenticated) {
return <div>Please log in to view this content</div>;
}
return <WrappedComponent {...props} />;
};
}
// Usage
const PrivateProfile = withAuth(UserProfile);
Discussion (5)
Great overview of modern patterns! I've been using custom hooks extensively but haven't explored compound components much. The example you provided makes it look really useful for building reusable UI kits.
Would love to see a more detailed comparison between Context+useReducer and Redux Toolkit. When would you recommend one over the other?