Last active
October 30, 2021 20:58
-
-
Save crtl/0cf85fc4b5253177513498c266b4b422 to your computer and use it in GitHub Desktop.
React MultiProvider
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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