DEV Community

Cover image for Modern HOCs with Hooks and Context API
Dennis Kaffer
Dennis Kaffer

Posted on

Modern HOCs with Hooks and Context API

Using the Context API sometimes makes us do some abstractions to avoid repeating code declarations, It's possible to have abstractions only with hooks too, but we can have a similar result with more composition with HOCs.

For example, here we have a common context that we'll integrate with a HOC:

import {
  createContext,
  useReducer,
  useMemo,
  useContext,
  ReactNode
} from "react";

type ContextProps = {
  isLoading: boolean;
  showError: boolean;
};

export type GlobalContextProps = {
  state: ContextProps;
  dispatch: (a: Action) => void;
};

const initialState: ContextProps = {
  isLoading: false,
  showError: false
};

export enum ACTIONS {
  IS_LOADING = "IS_LOADING",
  SHOW_ERROR = "SHOW_ERROR"
}

export type Action = {
  type: ACTIONS;
  payload: boolean;
};

export const GlobalContext = createContext<GlobalContextProps>({
  state: initialState,
  dispatch: () => {}
});

const reducer = (state: ContextProps, action: Action) => {
  const { type, payload } = action;
  switch (type) {
    case ACTIONS.IS_LOADING:
      return {
        ...state,
        isLoading: payload
      };
    case ACTIONS.SHOW_ERROR:
      return {
        ...state,
        showError: payload
      };
    default:
      return state;
  }
};

interface IGlobalProvider {
  children: ReactNode;
}

export const GlobalProvider = ({ children }: IGlobalProvider) => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const store = useMemo(() => ({ state, dispatch }), [state, dispatch]);

  return (
    <GlobalContext.Provider value={store}>
     {children}
    </GlobalContext.Provider>
  );
};

export const GlobalConsumer = GlobalContext.Consumer;

export const useGlobal = () => {
  const context = useContext(GlobalContext);
  if (!context) {
    throw new Error("useGlobal must be used after an GlobalContext.Provider");
  }
  return context;
};

Enter fullscreen mode Exit fullscreen mode

Using HOC as a Context API container

Here we have an example of how to abstract the useGlobal hook from the context and add some new functions like requestHandler who is responsible to make requests and update the state of context.
We can encapsulate all the context updates and make selectors for complex states.

import { FC, useCallback } from "react";
import { useGlobal, ACTIONS, GlobalContextProps } from "../contexts/global";

export interface IGlobal extends GlobalContextProps {
  requestHandler: (requestFunction: () => Promise<void>) => void
}

interface IWrappedComponent {
  global: IGlobal;
}

export const withGlobal = (WrappedComponent: FC<IWrappedComponent>) => {
  const HOC = () => {
    const { state, dispatch } = useGlobal();

    const requestHandler = useCallback(
      async (requestFunction) => {
        try {
          dispatch({ type: ACTIONS.IS_LOADING, payload: true });
          return await requestFunction();
        } catch (error) {
          dispatch({ type: ACTIONS.SHOW_ERROR, payload: true });
        } finally {
          dispatch({ type: ACTIONS.IS_LOADING, payload: false });
        }
      },
      [dispatch]
    );

    const props: IGlobal = {
      state,
      dispatch,
      requestHandler
    };

    return <WrappedComponent global={props} />;
  };

  return HOC;
};
Enter fullscreen mode Exit fullscreen mode

HOC usage

To use the HOC above we just call withGlobal function and pass the component as a param.

import { useCallback, useEffect, useState } from "react";
import { withGlobal, IGlobal } from "../hoc/withGlobal";

interface IProps {
  global: IGlobal;
}

const url = "https://hacker-news.firebaseio.com/v0";

const Page = ({ global: { requestHandler } }: IProps) => {
  const [posts, setPosts] = useState<any>([]);

  const getPosts = useCallback(
    () =>
      requestHandler(async () => {
        const response = await fetch(`${url}/topstories.json`);
        const data = await response.json();
        const requests = data.slice(0, 10).map(async (id: number) => {
          const post = await fetch(`${url}/item/${id}.json`);
          return await post.json();
        });

        const result = await Promise.all(requests);
        setPosts(result);
      }),
    [requestHandler]
  );

  useEffect(() => {
    getPosts();
  }, [getPosts]);

  return (
    <div className="App">
      <h1>Top 10 articles of Hacker News</h1>
      <ul>
        {posts.map((p: any) => (
          <li key={p.id}>
            <a href={p.url} title={p.title}>
              {p.title}
            </a>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default withGlobal(Page);
Enter fullscreen mode Exit fullscreen mode

In the Page component we can use all withGlobal features and we can focus on logical and render issues.
Using HOC in this case gave us a cleaner code and we don't need to worry about updating the global state.

Example in CodeSandbox

Conclusion

HOCs can be very useful to avoid code replication and calling the dispatch function too many times, they can be used as a bridge for components and context.
It's necessary to analyze the performance and if using HOCs in the application makes sense, in the vast majority of cases HOCs are usually a good choice.

In some future post I'll show you how to chain multiple HOCs and avoid props collisions. Thank you for reading.

Discussion (0)