React has completely changed the process of building user interfaces for developers. In a nutshell, React teaches us the concept of components—namely, self-contained, reusable pieces of the UI. Designing reusable but maintainable and scalable components is the nucleus of any React application. In this blog post, we are going to examine several key component design patterns that will help you build sturdy and reusable UI components.
Component design patterns are standardized methods of creating and organizing components to solve specific problems in UI design and architecture. These patterns offer a structured way of managing component logic, data flow, and UI composition to make it easier to build complex applications with a clean and maintainable codebase.
One of the basic design patterns in React is separating presentational and container components.
Presentational components are mainly concerned with how things look. They are mostly stateless and focus on rendering UI based on props. These components receive data and callbacks as props and do not handle data-fetching logic or state management.
Example:
const Button = ({ label, onClick }) => {
//
return <button onClick={onClick}>{label}</button>;
};
Container components manage the state and logic of your application. They fetch data, handle stateful logic, and pass data down to presentational components.
Example:
class ButtonContainer extends React.Component {
handleClick = () => {
console.log('Button clicked!');
};
render()
return <Button label="Click Me" onClick={this.handleClick} />;
}
}
Higher-order components are a high-level pattern for the reuse of the logic of a component. A higher-order component is a function that accepts a component and returns a new component, maybe with extended functionality.
Imagine you wish to add loading functionality. You can create an HOC to handle the loading state.
const withLoading = (WrappedComponent) => {
return class extends React.Component {
``
state = { loading: true };
componentDidMount() {
setTimeout(() => this.setState({ loading: false }), 2000); // Simulate API call
}
render()
return this.state.loading? <div>Loading.</div> : <WrappedComponent {.this.props} />;
}
};
};
const MyComponent = () => <div>Data Loaded!</div>;
const MyComponentWithLoading = withLoading(MyComponent);
Render props is a pattern for sharing code between components using a prop that is a function. This function will return a React element, allowing for greater flexibility.
You can create a simple counter that uses render props to control the displayed count.
class Counter extends React.Component {
state = { count: 0 };
increment = () => {
this.setState((prevState) => ({ count: prevState.count + 1 }));
};
render() {
return this.props.render(this.state.count, this.increment);
}
}
const App = () => (
<Counter render={(count, increment) => (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
)} />
);
The compound component pattern is a set of components working together. This pattern is quite useful when you want to create a more complex component with subcomponents that are tightly related.
Imagine a tab component: Tab
and TabPanel
.
const Tabs = ({ children }) =>
const [activeIndex, setActiveIndex] = React.useState(0);
return (
<div>
<div>
{React.Children.map(children, (child, index) =>
React.cloneElement(child, {
isActive: index === activeIndex,
onClick: () => setActiveIndex(index),
})
)}
</div>
<div>
{React.Children.toArray(children)[activeIndex].props.children}
</div>
</div>
);
};
const Tab = ({ isActive, onClick, children }) => (
<button style={{ fontWeight: isActive? 'bold' : 'normal' }} onClick={onClick}>
{children}
</button>
);
Hooks is extracted into React to be able to move the stateful logic around the code without affecting the component structure. Custom hooks are great for encapsulation of logics.
Custom hook to use form's state.
const useForm = (initialValues) => {
const [values, setValues] = React.useState(initialValues);
const handleChange = (event) =>
setValues((prevValues) => ({
.prevValues,
[event.target.name]: event.target.value,
}));
};
return [values, handleChange];
};
const MyForm = () => {
const [formValues, handleChange] = useForm({ name: '', email: '' });
return (
<form>
<input
name="name"
value={formValues.name}
onChange={handleChange}
/>
<input
name="email"
value={formValues.email}
onChange={handleChange}
/>
</form>
)
);
};
Crafting reusable UI components in React requires understanding and applying design patterns to promote code reusability, maintainability, and scalability. Whether you opt for presentational and container components, HOCs, render props, compound components, or hooks, each pattern has its unique benefits that can improve your development process.
With these component design patterns in hand, you will become empowered to build complex applications even more efficiently as React continues to grow. Accept these patterns and begin experimenting with them within your projects, and see your components become flexible and easy to maintain.