DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 16: Higher Order Components

Introduction

Higher Order Components (HOCs) are an advanced pattern in React that emerges from React’s compositional nature. They are not part of the React API but rather a design pattern that leverages React’s component model.

What is a Higher Order Components?

A Higher Order Component is a function that takes a component and returns a new component. It’s similar to higher-order functions in JavaScript, which take functions as arguments and/or return functions.

const EnhancedComponent = higherOrderComponent(WrappedComponent);

HOCs are a way to reuse component logic. While React components transform props into UI, higher-order components transform a component into another component.

Why Use Higher Order Components?

HOCs allow you to:

  • Abstract and reuse component logic
  • Add additional props to components
  • Override or extend existing behavior
  • Apply cross-cutting concerns like logging, authentication, or data fetching

In essence, HOCs help you avoid duplicating code across your React application and promote better separation of concerns.

Understanding the HOC Pattern

Let’s break down the HOC pattern by examining its structure and behavior.

Core Concepts

  1. Pure Function: An HOC is a pure function with zero side effects.
  2. Doesn’t Modify the Input: HOCs don’t modify the wrapped component; they create a new one.
  3. Composition: HOCs compose the original component by wrapping it in a container component.

The HOC Signature

The typical signature of an HOC looks like this:

function withFeature(WrappedComponent) {
  return class extends React.Component {
    // New functionality here
   
    render() {
      // Pass all props to the wrapped component
      return <WrappedComponent {...this.props} />;
    }
  };
}

This pattern creates a new component that renders the wrapped component, potentially adding props or behavior.

Creating Your First HOC

Let’s create a simple HOC that adds a loading state to any component:

// withLoading.js
import React, { useState, useEffect } from 'react';

function withLoading(WrappedComponent, loadingMessage = 'Loading...') {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) {
      return <div className="loading-indicator">{loadingMessage}</div>;
    }
   
    return <WrappedComponent {...props} />;
  };
}

export default withLoading;

Now let’s use this HOC with a component:

// UserList.js
import React from 'react';
import withLoading from './withLoading';

function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// Enhanced component with loading functionality
export default withLoading(UserList);

When using the enhanced component:

// App.js
import React, { useState, useEffect } from 'react';
import UserList from './UserList';

function App() {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
 
  useEffect(() => {
    // Simulate API call
    setTimeout(() => {
      setUsers([
        { id: 1, name: 'John Doe' },
        { id: 2, name: 'Jane Smith' },
      ]);
      setIsLoading(false);
    }, 2000);
  }, []);
 
  return <UserList isLoading={isLoading} users={users} />;
}

This HOC effectively abstracts the loading logic away from our components, making them more focused on their specific responsibilities.

Common Use Cases for HOCs

Higher-Order Components are versatile and can be used in many scenarios:

1. Authentication and Authorization

function withAuth(WrappedComponent) {
  return function WithAuth(props) {
    const { isAuthenticated, user } = useAuth(); // Custom hook for auth
   
    if (!isAuthenticated) {
      return <Navigate to="/login" />;
    }
   
    return <WrappedComponent {...props} user={user} />;
  };
}

// Usage
const ProtectedDashboard = withAuth(Dashboard);

2. Data Fetching

function withData(WrappedComponent, fetchData) {
  return function WithData(props) {
    const [data, setData] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError] = useState(null);
   
    useEffect(() => {
      const fetchDataAndUpdate = async () => {
        try {
          setIsLoading(true);
          const result = await fetchData();
          setData(result);
        } catch (err) {
          setError(err);
        } finally {
          setIsLoading(false);
        }
      };
     
      fetchDataAndUpdate();
    }, []);
   
    return (
      <WrappedComponent
        data={data}
        isLoading={isLoading}
        error={error}
        {...props}
      />
    );
  };
}

// Usage
const fetchUsers = () => fetch('/api/users').then(res => res.json());
const UsersWithData = withData(UserList, fetchUsers);

3. Styling and Theming

function withTheme(WrappedComponent) {
  return function WithTheme(props) {
    const theme = useContext(ThemeContext);
   
    return <WrappedComponent theme={theme} {...props} />;
  };
}

// Usage
const ThemedButton = withTheme(Button);

4. Logging and Analytics

function withTracking(WrappedComponent, componentName) {
  return function WithTracking(props) {
    useEffect(() => {
      // Log component mount
      analytics.logEvent(`${componentName}_mounted`);
     
      return () => {
        // Log component unmount
        analytics.logEvent(`${componentName}_unmounted`);
      };
    }, []);
   
    const trackEvent = (eventName, data) => {
      analytics.logEvent(`${componentName}_${eventName}`, data);
    };
   
    return <WrappedComponent {...props} trackEvent={trackEvent} />;
  };
}

// Usage
const TrackedUserList = withTracking(UserList, 'UserList');

Best Practices and Conventions

When working with HOCs, follow these best practices to avoid common pitfalls:

1. Use Descriptive Display Names

To aid debugging, set a descriptive display name that identifies the HOC:

function withAuth(WrappedComponent) {
  function WithAuth(props) {
    // ... implementation
  }
 
  WithAuth.displayName = `WithAuth(${
    WrappedComponent.displayName || WrappedComponent.name || 'Component'
  })`;
 
  return WithAuth;
}

2. Don’t Mutate the Original Component

Create a new component instead of modifying the input component:

// Bad - mutating the original component
function badHOC(WrappedComponent) {
  WrappedComponent.prototype.componentDidMount = function() {
    // Do something
  };
  return WrappedComponent;
}

// Good - creating a new component
function goodHOC(WrappedComponent) {
  return class extends React.Component {
    componentDidMount() {
      // Do something
    }
   
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

3. Pass Unrelated Props Through

Make sure your HOC passes through props that are unrelated to its specific concern:

function withAuth(WrappedComponent) {
  return function WithAuth(props) {
    const { isAuthenticated, user } = useAuth();
   
    // Pass all props to the wrapped component, plus the user if authenticated
    return isAuthenticated ? (
      <WrappedComponent {...props} user={user} />
    ) : (
      <Navigate to="/login" />
    );
  };
}

4. Maximize Composability

Design HOCs to be composable with other HOCs:

// Compose multiple HOCs
const EnhancedComponent = withAuth(withData(withTheme(MyComponent)));

// Or using a compose utility
import { compose } from 'redux';

const enhance = compose(
  withAuth,
  withData,
  withTheme
);
const EnhancedComponent = enhance(MyComponent);

5. Use the Spread Operator for Props

Always use the spread operator to pass all props to the wrapped component:

function withExtraProps(WrappedComponent) {
  return function WithExtraProps(props) {
    const extraProp = 'This is an extra prop';
   
    // Pass all original props plus the extra one
    return <WrappedComponent extraProp={extraProp} {...props} />;
  };
}

Composing Multiple HOCs

When your application grows, you might need to apply multiple HOCs to a single component. Let’s explore how to compose HOCs effectively.

Manual Composition

You can compose HOCs manually by nesting the function calls:

const EnhancedComponent = withAuth(withData(withTheme(BaseComponent)));

However, this can become hard to read with many HOCs.

Using Composition Utilities

Libraries like Redux provide a compose utility to make this more readable:

import { compose } from 'redux';

const enhance = compose(
  withAuth,
  withData,
  withTheme
);
const EnhancedComponent = enhance(BaseComponent);

You can also create your own compose function:

function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg;
  }
 
  if (funcs.length === 1) {
    return funcs[0];
  }
 
  return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

Understanding the Order of Composition

It’s important to understand that composition happens from right to left. In the example:

const EnhancedComponent = withAuth(withData(withTheme(BaseComponent)));
  1. First, withTheme is applied to BaseComponent
  2. Then withData is applied to the result of withTheme(BaseComponent)
  3. Finally, withAuth is applied to the result of withData(withTheme(BaseComponent))

This order matters especially when HOCs depend on each other or modify similar props.

HOCs vs. Hooks

With the introduction of React Hooks in React 16.8, many use cases for HOCs can now be solved more elegantly with custom hooks. Let’s compare the two approaches:

HOC Approach

// HOC for adding theme
function withTheme(WrappedComponent) {
  return function WithTheme(props) {
    const theme = useContext(ThemeContext);
    return <WrappedComponent theme={theme} {...props} />;
  };
}

// Usage
const ThemedButton = withTheme(Button);
function App() {
  return <ThemedButton onClick={() => alert('Clicked')} />;
}

Hook Approach

// Custom hook for theme
function useTheme() {
  return useContext(ThemeContext);
}

// Usage
function Button(props) {
  const theme = useTheme();
  return <button style={{ color: theme.primary }} {...props} />;
}

function App() {
  return <Button onClick={() => alert('Clicked')} />;
}

When to Use HOCs vs. Hooks

Use HOCs when:

  • You need to share the exact same component logic across many components
  • You want to inject props or modify the component tree
  • You’re working with class components
  • You need to override lifecycle methods

Use Hooks when:

  • You want to reuse stateful logic, not the entire component
  • You want to avoid the “wrapper hell” that can happen with nested HOCs
  • You prefer function components
  • You want more direct and readable code

In modern React applications, hooks are generally preferred for their simplicity and flexibility, but HOCs still have their place, especially in larger applications or when working with legacy code.

Debugging HOCs

Debugging components wrapped in multiple HOCs can be challenging. Here are some strategies to make debugging easier:

1. Use React DevTools

React DevTools can help you inspect the component hierarchy. When using HOCs, the component hierarchy becomes deeper, which can make debugging harder. Using proper display names helps identify components in the DevTools.

2. Static Methods and refs

HOCs can obscure static methods and refs of the wrapped component. To solve this:

  • For static methods, either hoist them explicitly or use the hoist-non-react-statics library:
import hoistNonReactStatics from 'hoist-non-react-statics';

function withAuth(WrappedComponent) {
  function WithAuth(props) {
    // ... implementation
  }
 
  return hoistNonReactStatics(WithAuth, WrappedComponent);
}
  • For refs, use the React.forwardRef API:
function withAuth(WrappedComponent) {
  const WithAuth = React.forwardRef((props, ref) => {
    // ... implementation
    return <WrappedComponent ref={ref} {...props} />;
  });
 
  WithAuth.displayName = `WithAuth(${
    WrappedComponent.displayName || WrappedComponent.name || 'Component'
  })`;
 
  return WithAuth;
}

3. Debugging Props

Add logging to see which props are being passed at each level:

function withLogging(WrappedComponent) {
  return function WithLogging(props) {
    console.log(`[WithLogging] Props:`, props);
    return <WrappedComponent {...props} />;
  };
}

Real-World Examples

Let’s examine some real-world examples of HOCs used in popular React libraries:

1. Redux’s connect

Redux’s connect function is a HOC that connects a React component to the Redux store:

import { connect } from 'react-redux';

function mapStateToProps(state) {
  return {
    todos: state.todos
  };
}

function mapDispatchToProps(dispatch) {
  return {
    addTodo: text => dispatch({ type: 'ADD_TODO', text })
  };
}

// TodoList is wrapped with the connect HOC
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

2. React Router’s withRouter

React Router’s withRouter HOC injects router props into your component:

import { withRouter } from 'react-router-dom';

function UserProfile({ match, history, location }) {
  const userId = match.params.id;
 
  return (
    <div>
      <h1>User Profile for ID: {userId}</h1>
      <button onClick={() => history.push('/users')}>Back to Users</button>
    </div>
  );
}

export default withRouter(UserProfile);

Note: In newer versions of React Router (v6+), this HOC has been replaced with hooks.

3. Recompose Library

Before hooks, the Recompose library provided a collection of HOCs for common use cases:

import { compose, withState, withHandlers } from 'recompose';

const enhance = compose(
  withState('count', 'setCount', 0),
  withHandlers({
    increment: ({ count, setCount }) => () => setCount(count + 1),
    decrement: ({ count, setCount }) => () => setCount(count - 1)
  })
);

function Counter({ count, increment, decrement }) {
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

export default enhance(Counter);

Summary

Higher-Order Components are a powerful pattern for code reuse in React applications. Key takeaways from this chapter include:

  1. HOCs are functions that take a component and return a new enhanced component
  2. They allow you to abstract and reuse component logic across your application
  3. Common use cases include authentication, data fetching, styling, and analytics
  4. Best practices include using descriptive names, not mutating the original component, and passing unrelated props through
  5. HOCs can be composed to apply multiple enhancements to a single component
  6. In modern React development, hooks often provide a simpler alternative to HOCs, but both have their place

Understanding HOCs is essential for working with many React libraries and for creating maintainable, reusable code in complex React applications.

Exercises

  1. Basic HOC Implementation
    • Create a withLogger HOC that logs when a component mounts and unmounts
    • Apply it to a simple component and verify the logs in the console
  2. HOC with Configuration
    • Create a withDelay HOC that renders a component after a specified delay
    • Make the delay time configurable through parameters
  3. Composing HOCs
    • Create three different HOCs: withLoading, withError, and withDataFetching
    • Compose them together to create a component that handles data fetching, loading states, and error states
  4. Converting HOCs to Hooks
    • Take one of your HOCs from the previous exercises
    • Implement the same functionality using a custom hook
    • Compare the two approaches and note the differences
  5. Real-World Application
    • Create a small app with protected routes using a withAuth HOC
    • Add analytics tracking with a withTracking HOC
    • Apply both HOCs to your components and ensure they work correctly together

Additional Resources

Documentation

Libraries

Articles and Tutorials

GitHub Repositories

Scroll to Top