Bayan Bennett

3 Things Every React Context Should Have | DevLog 008

TypeScript
React JS
Video version of this post

I've been using a pattern for React.Context that has worked well for me and I thought it would be something worth sharing.

Folder Structure

Create a new folder to house the projects' context variable. This way all the contexts that are in use in a project can be neatly accessible from the same folder.

I'll be making a folder for my context since I'll be adding more files to it in the future. The context that I will be creating is a facade for communicating with the future code runner that will run on a web worker. I'll be creating an ~/src/contexts/code-runner/index.tsx file, but if you don't have multiple files exclusive to the context, there's no need to create a separate folder, just put your file directly inside the ~/src/contexts folder.

Creating the Context

Create a context using React.createContext(). I put the initial value of the context to {} just as a placeholder. I also like to add displayName wherever I can as it will show up with that name when using the React Dev Tools extension in your browser.

const CodeRunnerContext = createContext({});
CodeRunnerContext.displayName = "CodeRunner";

Notice that I don't export the code runner context and there's a good reason for that. I want to have control over how this context is used. All components using the context should be using it the same way.

To make this happen, I always export a hook to consume the context and a higher-order component HOC to provide the context.

Custom Hook

Implementing a custom hook with useContext is painless. Just create a new function that will return a useContext.

export const useCodeRunner = () => useContext(CodeRunnerContext);

Custom Higher Order Component (HOC)

Just as hooks have a convention of beginning with "use". Higher order components have a convention of beginning with "with". The first function accepts a React component, the function that is returned is another React function component that wraps the first component in a context provider.

export const withCodeRunner = (Component) => (props) => {
  return (
    <CodeRunnerContext.Provider value={{}}>
      <Component {...props} />
    </CodeRunnerContext.Provider>
  );
};

Usage Example

Let's say we have a component, AppComponent, it's a basic component that returns an empty React fragment. All we'd need to do to get a context in this component is to add our custom hook useCodeRunner, and then wrap the component with our custom HOC, withCodeRunner.

const AppComponent = withCodeRunner(() => {
  const codeRunner = useCodeRunner();
  return <></>
});

Using this pair you can quickly add context to any component.

Another common pattern is to only wrap exported components with the HOC. This way the code becomes even cleaner.

const AppComponent = () => {
  const codeRunner = useCodeRunner();
  return <></>
};
export const App = withCodeRunner(AppComponent);

In this case, one thing to note is that the AppComponent won't work on its own if you use it within the same file. You will always need to use the App component as it has the context provided.

TypeScript Typing

For the useCodeRunner hook, it should inherit the type of the context.

For the HOC, since I'm wrapping a function component and returning a function component, all that is needed is to pass the props that the wrapped component expects to the returned component.

type WithCodeRunner = <T>(
  Component: FunctionComponent<T>
) => FunctionComponent<T>;

export const withCodeRunner: WithCodeRunner = (Component) => (props) => { /* ... */ };

TL; DR

Contexts should be in their own files and grouped together in a contexts folder. Contexts should only export two things, a hook to consume the context and a higher-order component to provide the context.

© 2022 Bayan Bennett