Skip to content

Instantly share code, notes, and snippets.

@crtl
Last active October 30, 2021 20:58
Show Gist options
  • Save crtl/0cf85fc4b5253177513498c266b4b422 to your computer and use it in GitHub Desktop.
Save crtl/0cf85fc4b5253177513498c266b4b422 to your computer and use it in GitHub Desktop.
React MultiProvider
import {render} from "@testing-library/react";
import {MultiProvider, Provider} from "./multi-provider";
import {FC} from "react";
describe("<MultiProvider />", () => {
it("should render children with empty list of providers", () => {
const wrapper = render(<MultiProvider providers={[]}>
<h1>Child</h1>
</MultiProvider>);
expect(wrapper.getByText("Child")).toBeInTheDocument();
});
it("should render provider with props", () => {
const wrapper = render(<MultiProvider providers={[
Provider((props) => {
return <div>
<p>Foo: {props.foo}</p>
{props.children}
</div>
}, {foo: "Bar"})
]}>
<p>Child</p>
</MultiProvider>);
expect(wrapper.getByText("Foo: Bar")).toBeInTheDocument();
expect(wrapper.getByText("Child")).toBeInTheDocument();
});
it("should render with list of providers in the same order they were passed", () => {
const First: FC = (props) => <div>
<h1>First</h1>
{props.children}
</div>;
const Second: FC = (props) => <div>
<h2>Second</h2>
{props.children}
</div>;
const Third: FC<{foo: string}> = (props) => <div>
<h3>Third</h3>
<p>Foo: {props.foo}</p>
{props.children}
</div>;
const providers = [
Provider(First),
Provider(Second),
Provider(Third, {foo: "Bar"}),
];
const wrapper = render(<MultiProvider providers={providers}>
<div>Child</div>
</MultiProvider>);
expect(wrapper.getByText("Child")).toBeInTheDocument();
const headings = ["First", "Second", "Third"];
// Test if headings were rendered
headings.forEach((text, i) => {
const element = wrapper.getByText(text);
expect(element).toBeInTheDocument();
// If last heading return
if (i === headings.length - 1) return;
// Test that child heading was rendered as children
const nextText = headings[i + 1];
const nextHeading = element.parentElement!.querySelector("h" + (i + 2));
expect(nextHeading).toBeInTheDocument();
expect(nextHeading!.textContent).toEqual(nextText);
});
// Test if prop was passed and rendered
expect(wrapper.getByText("Foo: Bar")).toBeInTheDocument();
wrapper.debug();
});
});
import React, {ComponentClass, FC, ReactElement} from "react";
type Component<T> = FC<T> | ComponentClass<T>;
/**
* MultiProviderProps
*/
export type MultiProviderProps = {
providers: ProviderData[];
};
/**
* ProviderData structure
*/
export type ProviderData<T = any> = {
component: Component<T>;
props?: T;
};
/**
* Helper to get type inference when passing providers
* @param component
* @param props
* @constructor
*/
export function Provider<T>(component: Component<T>, props?: T): ProviderData<T> {
return {component, props};
}
/**
* Takes a list of providers and renders them in the order they where provided to provide a flat api to manage context providers:
*
* Instead of this:
*
* ```
* export default function App() {
* return <FirstContextProvider>
* <SecondContextProvider>
* <ThirdContextProvider>
* <h1>My app</h1>
* </ThirdContextProvider>
* </SecondContextProvider>
* </FirstContextProvider>
* }
* ```
*
* Write this:
*
* ```
* export default function App() {
* return <MultiProvider providers={[
* Provider(FirstContextProvider)
* Provider(SecondContextProvider)
* Provider(ThirdContextProvider, {}) // pass optional props
* ]}>
* <h1>My app</h1>
* </MultiProvider>
* };
* ```
* @param props
* @constructor
*/
export const MultiProvider: FC<MultiProviderProps> = (props) => {
/**
* Renders a {provider} with a list of {providers} as children
* @param provider
* @param providers
*/
const renderProviders = (
provider: ProviderData,
providers: ProviderData[]
): ReactElement => {
if (!providers?.length) {
// Render last provider with props.children as children
return React.createElement(
provider.component,
provider.props,
props.children
);
}
// Render provider with remaining providers as children
providers = providers.slice(0);
const nextProvider = providers.shift()!;
return React.createElement(
provider.component,
provider.props,
// Recursively call renderProvider with next provider and other remaining providers as children
renderProviders(nextProvider, providers)
);
};
const providers = props.providers.slice(0);
if (!providers.length) {
return <>{props.children}</>;
}
return renderProviders(providers.shift()!, providers);
};
MultiProvider.displayName = "MultiProvider";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment