Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save phatnguyenuit/68122170e317d13e7148c7563be021b6 to your computer and use it in GitHub Desktop.
Save phatnguyenuit/68122170e317d13e7148c7563be021b6 to your computer and use it in GitHub Desktop.
How to compose React Providers with TypeScript

How to compose React Providers with TypeScript

Tree and sunset

Photo by Sergei A on Unsplash

Hi guys 😁! Long time no new articles!

Today, I am going to show you how to compose React providers with TypeScript.

Ready? let's go!

Prerequisites

  • Have ReactJS projects and use React Contexts
  • Have TypeScript projects to understand how typings are structured and work in the development environment

If you don't have these two items, it may become a bit hard for you to understand what is covered by this article.

Problem statement

React is still trendy nowadays in general, and using React Context APIs has become a common pattern to share global data between components as well.

In real-life React applications, we may have a lot of React Context Providers in the root component (App component for example). They become deeply JSX tree, hard to read, maintain, and test.

<Provider1>
  <Provider2>
    <Provider3>
      <Provider4 value={someValue}>
        <App />
      </Provider4>
    </Provider3>
  </Provider2>
</Provider1>

Is there any way to simplify the above JSX tree, just App and all context providers in only one Provider? 🤔

<AllInOneProvider>
  <App />
</AllInOneProvider>

Solution

There are some ways to resolve the above problem:

  • Create an all-in-one hardcoded Provider that contains all providers we need
  • Create one Provider receives all providers as a property
  • Create one utility to compose all providers into one provider
  • ... more approaches I have not figured out :) ...

In this article, I will share about the third, the way we compose all providers into one provider.

To create a composing providers utility we have the below checklist to make sure all covered:

  • should compose all providers into one provider
  • should take into account every single provider props
  • should be implemented with high code quality and strong TypeScript types

1. Normalise nested JSX tree

<Provider1>
  <Provider2>
    <Provider3>
      <Provider4 value={someValue}>
        <App />
      </Provider4>
    </Provider3>
  </Provider2>
</Provider1>

becomes

<AllInOneProvider>
  <App />
</AllInOneProvider>

We can construct one array containing all providers from outside like below:

const providers = [Provider1, Provider2, Provider3, Provider4];

If there's a provided children (<App />), firstly we think about the last Provider Provider4

So initially we have:

<Provider4 value={someValue}>
  <App />
</Provider4>

Continue the loop, we go with Provider3, and so on until we reach out to the first Provider Provider1

In JavaScript, we can use Array.reduceRight to loop through the items from the last to the first item (right to left)

2. Dead simple implementation of composing utility

function composeProviders(providers) {
  const ProviderComponent = ({ children }) => {
    const initialJSX = <>{children}</>;

    return providers.reduceRight((prevJSX, CurrentProvider) => {
      return <CurrentProvider>{prevJSX}</CurrentProvider>;
    }, initialJSX);
  };

  return ProviderComponent;
}

3. Enhance with Provider props

Uplift providers to include Provider props

const providers = [
  {
    Component: Provider1,
  },
  {
    Component: Provider2,
  },
  {
    Component: Provider3,
  },
  {
    Component: Provider4,
    props: { value: "someValue" },
  },
];

composeProviders will become:

function composeProviders(providers) {
  const ProviderComponent = ({ children }) => {
    const initialJSX = <>{children}</>;

    return providers.reduceRight(
      (prevJSX, { Component: CurrentProvider, props = {} }) => {
        return <CurrentProvider {...props}>{prevJSX}</CurrentProvider>;
      },
      initialJSX
    );
  };

  return ProviderComponent;
}

4. Enhance with TypeScript types

  • Define a Provider type including Provider Component, and its props (if any)
import React from "react";

interface Provider<TProps> {
  Component: React.ComponentType<React.PropsWithChildren<TProps>>;
  props?: Omit<TProps, "children">;
}
  • Define types for composeProviders utility
function composeProviders<TProviders extends Array<Provider<any>>>(
  providers: TProviders
): React.ComponentType<React.PropsWithChildren> {
  const ProviderComponent: React.FunctionComponent<React.PropsWithChildren> = ({
    children,
  }) => {
    const initialJSX = <>{children}</>;

    return providers.reduceRight<JSX.Element>(
      (prevJSX, { Component: CurrentProvider, props = {} }) => {
        return <CurrentProvider {...props}>{prevJSX}</CurrentProvider>;
      },
      initialJSX
    );
  };

  return ProviderComponent;
}

At this step, we have TypeScript types for our utility but looks like it does not work well, because we may have different Context value types.

IDE should suggest the correct props for the given Provider instead of typing anything as you wish

In this situation, we will create one more function to prepare Provider component details for every single Provider

function createProvider<TProps>(
  Component: React.ComponentType<React.PropsWithChildren<TProps>>,
  props?: Omit<TProps, "children">
): Provider<TProps> {
  return { Component, props };
}

providers now can be:

const providers = [
  createProvider(Provider1),
  createProvider(Provider2),
  createProvider(Provider3),
  createProvider(Provider4, { value: "someValue" }),
];

Wrapping all parts together, we now have Provider which contains multiple Providers following the article goal.

const providers = [
  createProvider(Provider1),
  createProvider(Provider2),
  createProvider(Provider3),
  createProvider(Provider4, { value: "someValue" }),
];

const AllInOneProvider = composeProviders(providers);

// render App
return (
  <AllInOneProvider>
    <App />
  </AllInOneProvider>
);

Now, in the root component (App component) you can create only one Provider including all Providers.

This utility is flexible as well, so we can set up our component unit tests in case we just need some providers (not all)

import React from 'react';
import { render } from '@testing-library/react';

const TestProvider = composeProviders([...])

const setupTest = (testProps: TestComponentProps) => {
    return render(<TestComponent />, { wrapper: TestProvider });
}

Conclusion

To sum up, we have 4 steps to compose React providers with TypeScript:

  1. Normalise nested JSX tree
  2. Dead simple implementation of composing utility
  3. Enhance with Provider props
  4. Enhance with TypeScript types

With these 4 steps, we can extract common logic and share it everywhere to compose React providers across React projects.

I hope you guys can find it helpful and resolve your current concern on how to compose React providers with TypeScript.

See you next time! :-)

References

@mikevb3
Copy link

mikevb3 commented Oct 12, 2024

Great post!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment