Modernizing Old React Patterns With React Hooks

Learn how to replace HOCs, render props and container components with custom React hooks.

· 6 min read
Modernizing Old React Patterns With React Hooks

React hooks have changed the way we build frontends ever since their introduction in React 16.8.

In combination with functional components, they've greatly reduced the amount of boilerplate code we write and gave us a place to store our reusable logic.

Prior to hooks, where to store the shared logic used to be a difficult question without a clear answer. For that, the React community came up with various patterns, which nowadays have largely been obsoleted by React hooks.

Nonetheless, those patterns are still prevalent in many older articles, which causes confusion, especially among beginners.

This article will explain those patterns and show how React hooks have replaced them so that you're able to better navigate the ever-changing React landscape. You will be writing better front-end code afterwards.

1) Container-presentational components

This pattern was introduced to separate simple stateless UI components from those that use lifecycle methods or trigger side effects. It was largely popularized by the Redux framework and was sometimes called smart-dumb components.

At the time, if you wanted to hook into the lifecycle methods or even simply have some local state, you needed to write a class component, which generally involved writing more boilerplate and more complex code. The container-presentational component pattern helped with that.

The presentational component would receive anything it needed via props, so it could be a simple stateless functional component, that mostly contained JSX:

import React from "react";

const CommentList = comments => (
  <ul>
    {comments.map(({ body, author }) =>
      <li>{body}-{author}</li>
    )}
  </ul>
)

export default CommentList
The presentational component CommentList.js

Then, the container component would mostly contain the side-effect code (e.g. bind to global redux state) or lifecycle-related code (e.g. calling the APIs). You could then use it to wrap any compatible presentational component to reuse the logic:

import CommentList from "./CommentList";

class CommentListContainer extends React.Component {
  constructor() {
    super();
    this.state = { comments: [] }
  }
  
  componentDidMount() {
    fetch("/my-comments.json")
      .then(res => res.json())
      .then(comments => this.setState({ comments }))
  }
  
  render() {
    return <CommentList comments={this.state.comments} />;
  }
}
The container component CommentListContainer.js

That was an example of a container-presentational component pattern from 2015 by Michael Chan. It's now considered ancient in the internet years.

Example with functional components

Nowadays, you're unlikely to see class components in action. However, the pattern could be used with modern functional components too:

Example of the container-presentational components.

Replace the container with a custom hook

However, since our presentational component doesn't do much else than manage some state and register event handlers and effects, we can move all of it into a reusable custom hook:

function useFiles() {
  const [files, setFiles] = useState<File[] | undefined>();

  useEffect(() => {
    loadFiles().then(setFiles);
  }, []);

  const upload = async () => {
    const file = await uploadFile();
    setFiles((files) => (files ? [...files, file] : [file]));
  };

  return { files, upload };
}
A custom hook containing the container component logic.

Then we can use it together with the presentational component, wherever we need the logic:

function FileUploadApp() {
  const { files, upload } = useFiles();

  return (
    <div>
      <h1>Cloud Drive</h1>
      {files ? <Files files={files} onUploadClick={upload} /> : <div>Loading...</div>}
    </div>
  );
}

Using custom hooks instead of container components is a much simpler and more flexible way to share reusable logic between components. Here's the full code:

The example from before, only using the custom hook.

2) Higher-Order components (HOCs)

Higher-order component, also known as HOC, is a function that takes a component function as an argument and returns a new component function with some extra props or functionality.

They initially replaced mixins as a way to reuse logic between components, but nowadays it's easier to use hooks instead.

If you've used redux before react-toolkit and hooks, you will remember having to wrap your container components with a connect() function. That's an example of a higher-order component:

function Container(props) {
  // ...
}

const mapStateToProps = state => ({
  // ...
});

export default connect(mapStateToProps)(Container);
The old style of connecting components to redux.

Even though it's easier to use custom hooks for code reuse, the HOC pattern is still useful sometimes - unlike hooks, the HOC has its own scope and can wrap a component not only with logic but also with additional JSX.

Creating HOCs

To create a higher-order component you need to declare a function that accepts a component function as part of its arguments, and then return a new component function:

interface HocProps {
  // ...
}

interface InjectedProps {
  // ...
}

// 👇 Creates a HOC component that has accepts a component that requires OwnProps+InjectedProps
// The HOC will then inject the InjectedProps, but will additionally require HocProps
function createHOC<OwnProps>(Component: React.ComponentType<OwnProps & InjectedProps>) {
  const injectedProps: InjectedProps = {
    // ...
  };

  return (props: OwnProps & HocProps) => {
    // 💭 Do something with HocProps
    return <Component {...props} {...injectedProps} />;
  };
}
TypeScript example of a generic HOC.

The above is a framework for writing HOCs. The complexity comes from typescript usage. Strip out the types, and it becomes very simple. In fact, it's just a function closure.

HOC example

Here's the same "file upload" example from before, only using withFiles higher-order component:

The example from before, using a HOC.

It can be refactored to use a custom hook in the exact same way you would refactor the container component:

The same custom hook example from before. Incidentally, that's how you refactor a HOC too.

3) Render props

Render prop is a function prop that returns a react element when called. The component with the render prop might not even render any JSX itself, and call the render function instead:

<Component render={(props) => <div>some content</div>} />

The technique is used to separate business logic from rendering, often to delegate the rendering to the parent component. The render function gets access to the internal component state via props that are passed into it.

Render props example

Let's take the same "file upload" example from before, only this time with render props used to render the list and the upload button:

Render props used to render the list item and the button.

Replacing render props with custom hooks

At first glance, render props seem to let us customize how we render certain parts of the UI. They're not solving an issue of reusing logic. However, is that really so?

In the previous examples, we used the custom hook to share logic. Now, the reason render props is a function in the first place, and not simply JSX - is to gain access to the component's internal state.

We could move that internal state into a custom hook, lift it up to the component's parent, and then pass it down as props. Then we could use the same state from the hook to render whatever we would render in the render prop function.

This way we don't need the render props anymore, and instead can pass the rendered JSX as props:

The custom hook is used in the parent component to replace the render props.
💡
Note that the useFiles hook is exactly the same. We didn't even need to do anything special to remove the render props from our code.

Conclusion

I find it fascinating how custom hooks have replaced these three seemingly different patterns. Even more interesting is that the hook code is exactly the same in all three examples.

This hints to me that if we use custom hooks to share logic between components, we naturally don't need the container components, HOCs, or even render props.

Now when you read some older code examples, you can always reference the above three examples and see how that code could be rewritten using custom hooks. Use them, and your code will be cleaner and simpler.

As usual, find the code examples in my GitHub repository.


Continue your streak of learning advanced React concepts and read about how to use React portals in the real world:

Learn to Use React Portals in the Real World
What real-world problems do they solve? Learn by example.