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!
- 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.
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>
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
<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)
function composeProviders(providers) {
const ProviderComponent = ({ children }) => {
const initialJSX = <>{children}</>;
return providers.reduceRight((prevJSX, CurrentProvider) => {
return <CurrentProvider>{prevJSX}</CurrentProvider>;
}, initialJSX);
};
return ProviderComponent;
}
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;
}
- 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 });
}
To sum up, we have 4 steps to compose React providers with TypeScript:
- Normalise nested JSX tree
- Dead simple implementation of composing utility
- Enhance with Provider props
- 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! :-)
Great post!!