The Evolution of React Design Patterns: From HOCs to Hooks and Custom Hooks
Sam Abaasi
Posted on August 25, 2023
"To evolve is to find better ways of doing things." - Dan Abramov
In the early days of React development, developers faced a myriad of challenges. React applications were becoming increasingly complex, and issues related to component reuse, state management, and code maintainability emerged as common adversaries. React's core team, which boasts notable members like Dan Abramov, Sebastian Markbåge, Sophie Alpert, and Andrew Clark, recognized these challenges and set out on a quest to unearth better design patterns. This article takes you on a journey through the evolution of React design patterns, from the birth of High-Order Components (HOCs) to the groundbreaking era of React Hooks and the rise of Custom Hooks.
Chapter 1: High-Order Component (HOC) Pattern
Problems That Necessitated HOCs
In the early React days, component reuse was a formidable hurdle. Managing shared state was a labyrinthine task, and codebases were evolving into tangled webs of complexity. The React team, including the influential Dan Abramov, acknowledged these problems and embarked on a quest to find solutions.
Dan Abramov emphasized the issues with mixing data-fetching and rendering logic within components in his seminal article: "Extracting your components’ data dependencies lets you reuse presentational components with different data sources." This was a pivotal moment in the evolution of React patterns.
Enter High-Order Components (HOCs)
To address the issues at hand, the concept of High-Order Components (HOCs) was born. HOCs allowed developers to wrap components, thereby enhancing their functionality and reusability. With HOCs, shared logic and behavior could be abstracted and applied across the application.
Here's a real-world example of a simple HOC that adds authentication logic to a component:
import React, { Component } from 'react';
const withAuthentication = (WrappedComponent) => {
class WithAuthentication extends Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated: false,
};
}
componentDidMount() {
// Check authentication logic here
const isAuthenticated = // Your authentication check logic
this.setState({ isAuthenticated });
}
render() {
if (!this.state.isAuthenticated) {
return <div>Authentication failed. Please log in.</div>;
}
return <WrappedComponent {...this.props} />;
}
}
return WithAuthentication;
};
export default withAuthentication;
Pros of HOCs
HOCs bestowed React development with several advantages. They facilitated code reuse by enabling the wrapping of components with specific behaviors. For example, a withAuthentication
HOC could be used to protect routes requiring authentication. Developers reveled in the flexibility and composability that HOCs offered.
Dan Abramov: "Higher-Order Components (HOCs) allow you to reuse component logic. This is achieved by wrapping a component in a function that returns a new component with the desired behavior."
Cons and Weaknesses of HOCs
However, HOCs were not without their set of challenges. Wrapping components with HOCs led to an abundance of wrapper components, resulting in a complex hierarchy. Debugging and comprehending component structures became increasingly daunting, and the term "wrapper hell" found its place in React discourse.
Dan Abramov: "HOCs don’t solve every problem. They can make your code more complex by introducing unnecessary layers of abstraction, and they can be a little harder to understand."
Chapter 2: Presentation and Container Pattern
Transition to Presentation and Container
As the React community grappled with the limitations of HOCs, a new pattern emerged: the Presentation and Container pattern. This pattern sought to address the issues brought about by HOCs while preserving their strengths. It delineated components into two distinct categories: presentation components responsible for rendering, and container components responsible for data logic.
Here's an example of a presentation component:
import React from 'react';
const PresentationComponent = ({ data }) => (
<div>
<h1>Data Presentation</h1>
<p>{data}</p>
</div>
);
export default PresentationComponent;
And a container component that manages the state and logic:
import React, { Component } from 'react';
class ContainerComponent extends Component {
constructor(props) {
super(props);
this.state = {
data: '',
};
}
componentDidMount() {
// Fetch data and update state here
const data = // Your data fetching logic
this.setState({ data });
}
render() {
return <PresentationComponent data={this.state.data} />;
}
}
export default ContainerComponent;
Pros of Presentation and Container
The Presentation and Container pattern introduced clarity into React codebases. Presentation components evolved into pure, stateless components dedicated solely to rendering. Container components assumed the role of managing data logic and maintaining component state. This segregation simplified testing and bolstered code maintainability.
Sebastian Markbåge: "The goal of the presentation and container pattern is to improve reusability, testability, and the overall organization of your components."
Cons and Weaknesses of Presentation and Container
Nonetheless, this pattern was not a panacea. Developers frequently found themselves creating multiple container components for a single presentation component, resulting in an abundance of files and an escalation of complexity. The pattern, while an improvement, had its complexities and challenges to contend with.
Sophie Alpert: "Sometimes you may need to write a container component even when you don't feel like it's reusing any logic, and that's okay. It's not about how much logic you reuse, but how much you can understand at a glance."
Chapter 3: The Emergence of React Hooks
Introduction to React Hooks
The React landscape experienced a monumental shift with the advent of React Hooks. Hooks were envisioned as a remedy for the challenges posed by both HOCs and the Presentation and Container pattern. They empowered functional components to manage state and side effects without the need for class components.
Here's an example of a functional component using the useState
and useEffect
hooks:
import React, { useState, useEffect } from 'react';
const HooksComponent = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
export default HooksComponent;
Pros of React Hooks
React Hooks ushered in a new era of simplicity and reusability. They enabled functional components to manage local state, side effects, and context with remarkable ease. The concise syntax and elimination of class components resonated with developers, resulting in cleaner and more readable code.
Dan Abramov: "React Hooks aim to solve the problems of render props and higher-order components in a way that feels more natural and comes with fewer trade-offs."
Cons and Weaknesses of React Hooks
Despite their widespread acclaim, React Hooks were not entirely devoid of challenges. Adapting existing class components to functional components with hooks required effort and consideration. Additionally, certain lifecycle methods, such as getSnapshotBeforeUpdate
, remained exclusive to class components.
Sophie Alpert: "Hooks don't cover all use cases yet, and they might never cover all use cases. That's why we're not deprecating classes. The goal is to provide alternatives, not to force people to change their habits."
Chapter 4: The Ascendance of Custom Hooks
Custom Hooks as a Pattern
The React community's insatiable appetite for innovation paved the way for the Custom Hooks pattern. Custom Hooks represented a paradigm shift in React design patterns, enabling developers to encapsulate and share logic across components with unparalleled simplicity.
Here's an example of a custom hook that manages a toggle state:
import { useState } from 'react';
const useToggle = (initialValue = false) => {
const [value, setValue] = useState(initialValue);
const toggle = () => {
setValue(!value);
};
return [value, toggle];
};
export default useToggle;
Pros of Custom Hooks
Custom Hooks provided React developers with the ultimate tool for code organization and reuse. They unlocked the potential for logic abstraction and distribution, transforming complex components into concise compositions of custom hooks. The newfound modularity elevated React applications to new heights of maintainability.
Andrew Clark: "Custom Hooks let you extract component logic into reusable functions. This is especially useful for sharing behavior across components, such as form handling or animations."
Cons and Weaknesses of Custom Hooks
While Custom Hooks significantly enriched the React ecosystem, they were not a silver bullet. The proliferation of custom hooks across codebases warranted thoughtful naming and documentation. Developers needed to ensure that the purpose and usage of custom hooks were well-understood to prevent confusion.
Andrew Clark: "Custom Hooks can be a double-edged sword. When used appropriately, they can make your code more organized and reusable. However, misuse or excessive use can lead to confusion and unnecessary complexity."
Chapter 5: The Ongoing Evolution
As the React ecosystem matures, the landscape of design patterns and best practices continues to evolve. Let's take a closer look at how these patterns have adapted and explore real-world examples that demonstrate their ongoing evolution.
1. High-Order Component (HOC) Pattern
Evolution: Render Props
The HOC pattern, once a cornerstone of React development, has seen a shift toward more flexible patterns like Render Props. While HOCs remain relevant, Render Props offer a more intuitive way to share code between components.
Example:
Consider a common scenario where you need to fetch and display data. Instead of wrapping components with an HOC, you can use a Render Props component:
class DataFetcher extends React.Component {
state = {
data: null,
};
componentDidMount() {
// Fetch data and update the state
}
render() {
return this.props.render(this.state.data);
}
}
<DataFetcher render={(data) => <DisplayData data={data} />} />
Render Props provide greater component composability and reduce the nesting of higher-order components.
2. Presentation and Container Pattern
Evolution: Component Composition
While the Presentation and Container pattern emphasized separation of concerns, modern React development promotes component composition. Composing smaller, reusable components has become a prevailing practice.
Example:
Instead of creating a separate Container component for data fetching, you can compose your UI from smaller components:
function WeatherDisplay({ city }) {
const { data, error, loading } = useWeatherData(city);
if (loading) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorMessage error={error} />;
}
return <WeatherInfo data={data} />;
}
This approach enhances reusability and simplifies component hierarchies.
3. React Hooks
Evolution: Concurrent Mode
React Hooks represent a pivotal advancement, but the evolution doesn't stop there. Concurrent Mode, an experimental React feature, is set to further improve the user experience by allowing React to work on multiple tasks simultaneously.
Example:
Suppose you're building a resource-intensive application. Concurrent Mode can help prioritize rendering updates for critical components, ensuring a smoother user experience.
function ResourceIntensiveComponent() {
// Resource-intensive rendering logic
}
export default React.memo(ResourceIntensiveComponent, (prevProps, nextProps) => {
// Implement custom comparison logic
return prevProps.data === nextProps.data;
});
By implementing custom comparison logic, Concurrent Mode can focus resources where they're needed most.
4. Custom Hooks
Evolution: External Libraries and the Community
Custom Hooks have empowered developers to create their own libraries and share them with the community. As React's ecosystem expands, external libraries offer even more powerful and specialized solutions.
Example:
Imagine you're working on a project that requires complex state management. Instead of reinventing the wheel, you can leverage external libraries like Redux or Mobx, which provide robust state management solutions.
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
const count = useSelector((state) => state.counter);
const dispatch = useDispatch();
return (
<div>
<span>Count: {count}</span>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
</div>
);
}
By embracing external libraries and community-driven solutions, you can harness the collective expertise of the React community.
Conclusion
The journey through React design patterns serves as a testament to the dynamic nature of web development. From the early struggles of component reuse to the dawn of Hooks, React's evolution has been nothing short of extraordinary. As you navigate your React projects, remember the rich history of these patterns, and wield them to craft maintainable, efficient, and scalable applications.
Diverse Insights from React Team
For deeper insights into the React team's perspectives, delve into their articles and opinions:
Dan Abramov on Presentational and Container Components
Sebastian Markbåge on React Component Patterns
Sophie Alpert on Component Patterns and Composition
Andrew Clark on the Adoption and Impact of Hooks
These resources offer invaluable insights into the motivations and philosophies that shaped React's design patterns.
Posted on August 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 25, 2023