Design Patterns for Building Scalable React Applications

SHUBHAM GAUTAM
5 min readFeb 16, 2023

--

Make your React code more effective by using these 10 design patterns

Photo by Lautaro Andreani on Unsplash

React is one of the most popular front-end JavaScript libraries out there, and for good reason. With its component-based architecture, virtual DOM, and efficient rendering, React has become a go-to choice for web developers looking to build modern and scalable applications. However, like any tool, React is only as effective as the way you use it. In this article, we’ll explore the top design patterns that can help you build better, more maintainable React applications.

  1. Container/Presenter Pattern:

One of the most popular design patterns for React is the Container/Presenter pattern. This pattern separates your logic (Container) from your UI components (Presenter) and allows for a cleaner and more scalable codebase. Here’s an example:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchUsers } from './actions';
import UserList from './UserList';

class UserListContainer extends Component {
componentDidMount() {
this.props.fetchUsers();
}

render() {
return <UserList users={this.props.users} />;
}
}

const mapStateToProps = state => ({
users: state.users
});

const mapDispatchToProps = {
fetchUsers
};

export default connect(
mapStateToProps,
mapDispatchToProps
)(UserListContainer);

2. Higher-Order Components (HOCs):

Another powerful design pattern in React is Higher-Order Components (HOCs). HOCs allow you to wrap your components with additional functionality, such as logging, authentication, or caching.

Here’s an example:

import React from 'react';

const withLogger = WrappedComponent => {
class WithLogger extends React.Component {
componentDidMount() {
console.log(`Component ${WrappedComponent.name} mounted`);
}

render() {
return <WrappedComponent {...this.props} />;
}
}

return WithLogger;
};

const MyComponent = props => {
return <div>My Component</div>;
};

export default withLogger(MyComponent);

3. Render Props:

Render Props is a pattern that allows you to share code between components by passing a function as a prop. This allows for greater flexibility and reusability of your code.

Here’s an example:

import React from 'react';

class Toggle extends React.Component {
state = {
on: false
};

toggle = () => {
this.setState(prevState => ({ on: !prevState.on }));
};

render() {
return this.props.children({
on: this.state.on,
toggle: this.toggle
});
}
}

const MyComponent = () => (
<Toggle>
{({ on, toggle }) => (
<div>
{on ? 'On' : 'Off'}
<button onClick={toggle}>Toggle</button>
</div>
)}
</Toggle>
);

export default MyComponent;

4. Compound Components:

Compound Components is a pattern that allows you to create components that work together as a cohesive unit. This pattern can be especially useful when you have components that need to work together in a specific way, but you don’t want to expose the implementation details to the end user.

Here’s an example:

import React from 'react';

const Tabs = ({ children }) => {
const [activeIndex, setActiveIndex] = React.useState(0);

const handleTabClick = index => {
setActiveIndex(index);
};

return (
<div>
{React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
isActive: index === activeIndex,
onClick: () => handleTabClick(index)
});
})}
</div>
);
};

const Tab = ({ children, isActive, onClick }) => {
return (
<div onClick={onClick} style={{ fontWeight: isActive ? 'bold' : 'normal' }}>
{children}
</div>
);
};

const MyComponent = () => {
return (
<Tabs>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
<Tab>Tab 3</Tab>
</Tabs>
);
};

export default MyComponent;

5. The Context API:

The Context API is a built-in feature in React that allows you to share data between components without having to pass props down the component tree. This pattern can be especially useful when you have data that needs to be shared across multiple components.

Here’s an example:

mport React from 'react';

const ThemeContext = React.createContext('light');

const MyComponent = () => {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
};

const Toolbar = () => {
return (
<div>
<ThemedButton />
</div>
);
};

const ThemedButton = () => {
const theme = React.useContext(ThemeContext);
return <button style={{ color: theme === 'dark' ? 'white' : 'black', background: theme }}>{theme} Theme</button>;
};

export default MyComponent;

6. Controlled Components:

Controlled Components is a pattern that allows you to control the state of a component from its parent component. This pattern can be useful when you need to share state between components and keep your code organized.

Here’s an example:

import React from 'react';

const MyComponent = () => {
const [value, setValue] = React.useState('');

const handleChange = event => {
setValue(event.target.value);
};

return (
<div>
<input type="text" value={value} onChange={handleChange} />
<div>{value}</div>
</div>
);
};

export default MyComponent;

7. Presentational and Container Components:

Presentational and Container Components is a pattern that separates components into two categories: Presentational Components and Container Components. This pattern can be useful when you need to separate the concerns of your components and make your code more modular.

Here’s an example:

import React from 'react';

const MyComponent = ({ value, onChange }) => {
return (
<div>
<input type="text" value={value} onChange={onChange} />
<div>{value}</div>
</div>
);
};

class MyContainerComponent extends React.Component {
state = {
value: ''
};

handleChange = event => {
this.setState({ value: event.target.value });
};

render() {
return <MyComponent value={this.state.value} onChange={this.handleChange} />;
}
}

export default MyContainerComponent;

8. Error Boundaries:

Error Boundaries is a pattern that allows you to catch errors that occur in your components and handle them gracefully. This pattern can be useful when you want to prevent your application from crashing due to unexpected errors.

Here’s an example:

import React from 'react';

class ErrorBoundary extends React.Component {
state = {
hasError: false
};

static getDerivedStateFromError(error) {
return { hasError: true };
}

render() {
if (this.state.hasError) {
return <div>Something went wrong.</div>;
}

return this.props.children;
}
}

const MyComponent = () => {
throw new Error('Oops!');
return <div>My Component</div>;
};

const App = () => {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
};

export default App;

These design patterns are just a few of the many ways you can improve your React applications. By using these patterns, you can create more maintainable, scalable, and reusable code. So the next time you’re building a React application, consider using these patterns to make your code more effective.

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

If you like this show some love and checkout other stories here. Connect with me on twitter.

--

--