React
Higher Order Components
Async Methods
JavaScript
Web Development

React HOC pattern - dealing with async methods

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Higher-order components, or HOCs, are a React pattern for wrapping one component in another component to inject shared behavior. React's own legacy documentation notes that HOCs are not commonly used in modern React code, but they still exist in real codebases and third-party libraries. If you have to combine a HOC with async work, the safest design is to keep the async state in the wrapper and pass the result, loading state, and actions down as props.

What the HOC Should Own

An async-aware HOC usually owns three pieces of state:

  • the current async result
  • loading status
  • error status

It may also expose an async method that the wrapped component can call.

A clean pattern is to keep that logic in the wrapper and avoid mutating the wrapped component or hiding too much behavior inside global side effects.

jsx
1import React, { useCallback, useState } from "react";
2
3function withUserLoader(WrappedComponent) {
4  function WithUserLoader(props) {
5    const [user, setUser] = useState(null);
6    const [loading, setLoading] = useState(false);
7    const [error, setError] = useState(null);
8
9    const loadUser = useCallback(async (id) => {
10      setLoading(true);
11      setError(null);
12      try {
13        const response = await fetch(`/api/users/${id}`);
14        if (!response.ok) {
15          throw new Error(`Request failed: ${response.status}`);
16        }
17        const data = await response.json();
18        setUser(data);
19      } catch (err) {
20        setError(err);
21      } finally {
22        setLoading(false);
23      }
24    }, []);
25
26    return (
27      <WrappedComponent
28        {...props}
29        user={user}
30        loading={loading}
31        error={error}
32        loadUser={loadUser}
33      />
34    );
35  }
36
37  WithUserLoader.displayName = `WithUserLoader(${WrappedComponent.displayName || WrappedComponent.name || "Component"})`;
38  return WithUserLoader;
39}
40
41export default withUserLoader;

This keeps the wrapped component focused on rendering and user interaction.

Use the Injected Async Method in the Wrapped Component

jsx
1import React, { useEffect } from "react";
2
3function UserPanel({ userId, user, loading, error, loadUser }) {
4  useEffect(() => {
5    loadUser(userId);
6  }, [userId, loadUser]);
7
8  if (loading) return <p>Loading...</p>;
9  if (error) return <p>{error.message}</p>;
10  if (!user) return null;
11
12  return <h1>{user.name}</h1>;
13}
14
15export default UserPanel;

The wrapped component does not need to know where the data came from. It only consumes props.

Why This Pattern Works

The React legacy HOC guidance emphasizes composition rather than mutation. That matters even more with async behavior.

A mutation-based HOC that patches lifecycle methods or instance methods becomes hard to debug once loading, cancellation, and error handling are involved. A wrapper component is clearer because it keeps async state in one place and passes through unrelated props.

This also makes testing easier. You can render the wrapped component with fake loading, error, and loadUser props, or test the HOC boundary separately.

Avoid Recreating the HOC During Render

Another important React HOC rule is not to create enhanced components inside another component's render path.

jsx
1// Bad
2function Parent() {
3  const Enhanced = withUserLoader(UserPanel);
4  return <Enhanced userId="42" />;
5}

That recreates a new component type on every render and can cause remounts and state loss. Define the enhanced component once at module scope.

jsx
const UserPanelWithLoader = withUserLoader(UserPanel);

This matters a lot for async code because remounting can restart requests and drop in-flight state unexpectedly.

Modern React Context

In new React code, hooks and custom hooks are often a better default than HOCs for shared async logic. That does not make HOCs wrong. It just means you should use them deliberately, usually for legacy integration or library patterns already built around HOCs.

If you are already in a HOC-based codebase, the right goal is not to rewrite everything immediately. The goal is to make the HOC predictable:

  • wrapper owns async state
  • wrapped component gets explicit props
  • unrelated props pass through unchanged
  • side effects are isolated and testable

Common Pitfalls

The biggest mistake is hiding async side effects in a HOC while giving the wrapped component no clear loading or error contract.

Another issue is creating the HOC inside render, which causes remounts and resets async state.

Developers also sometimes mutate the wrapped component instead of composing it. That conflicts with React's recommended HOC conventions and becomes fragile with async behavior.

Finally, if you are writing brand-new React code, consider whether a custom hook is simpler than a HOC before adding another wrapper layer.

Summary

  • HOCs are still valid in some React codebases, but they are not the common modern default.
  • For async behavior, keep loading, error, and result state inside the wrapper.
  • Pass async methods and async state to the wrapped component as explicit props.
  • Define enhanced components once, not inside render.
  • Prefer composition and clear prop contracts over mutation or hidden side effects.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.