Skip to content

Instantly share code, notes, and snippets.

@sstackus
Last active June 24, 2024 16:05
Show Gist options
  • Save sstackus/f351ca6b3c0fed39d3f21e96a02a480c to your computer and use it in GitHub Desktop.
Save sstackus/f351ca6b3c0fed39d3f21e96a02a480c to your computer and use it in GitHub Desktop.
Compose React context providers

Example

See it in action at https://codesandbox.io/s/n6txl.

The Problem

Have you ever come across a tree like this:

<ContextA.Provider>
  <ContextB.Provider>
    <ContextC.Provider>
      ...
    </ContextC.Provider>
  </ContextB.Provider>
</ContextA.Provider>

Instead, we can compose/combine this list of providers:

<Compose items={[
  [ContextA, props],
  [ContextB, props],
  [ContextC, props],
]}>
  ...
</Compose>
import React from "react";
import Compose from "./Compose.js";
import Context from "./Context.js";
export default function App() {
const [foo] = React.useState(1);
const [bar] = React.useState(2);
return (
<Compose
items={[
[Context, { foo, bar }],
]}
>
<Context.Consumer />
</Compose>
);
}
import React from "react";
export default function Compose({ items, children }) {
return items.reduceRight(
(acc, [Context, props]) =>
React.createElement(Context.Provider, props, acc),
children
);
}
import React from "react";
const Context = React.createContext();
function Consumer() {
const value = React.useContext(Context);
return <pre>Value: {JSON.stringify(value)}</pre>;
// Value: {"foo":1,"bar":2}
}
function Provider({ children, ...props }) {
return <Context.Provider value={props}>{children}</Context.Provider>;
}
export default { Context, Provider, Consumer };
@natoehv
Copy link

natoehv commented Apr 14, 2023

thanks!!!, it was helpful to me, I'm using jest to test, now with this gist I can compose different wrapper depending of the test 👏 👏 👏

@malininss
Copy link

Hey! Here is a TS version. Could be helpful for someone:

interface ComponentComposerProps<T> {
  items: Array<[FC<T>, T] | FC<PropsWithChildren>>;
  children: ReactNode;
}

const ComponentComposer = <T extends PropsWithChildren>({
  items,
  children,
}: ComponentComposerProps<T>): ReactNode =>
  items.reduceRight<ReactNode>((acc, item) => {
    if (Array.isArray(item)) {
      const [Component, props] = item;

      return <Component {...props} />;
    }

    const Component = item;

    return <Component>{acc}</Component>;
  }, children);

@samMeow
Copy link

samMeow commented Jun 24, 2024

Or the tough way for typescript compose

type JEC<T> = JSXElementConstructor<T>;
type CP<T extends keyof JSX.IntrinsicElements | JEC<any>> = Partial<ComponentProps<T>>;
interface ComposeFn {
  <
    A extends JEC<any>,
    B extends JEC<any>,
    C extends JEC<any>,
    D extends JEC<any>,
    E extends JEC<any>,
    F extends JEC<any>,
  >(p: {
    providers: [[A, CP<A>], [B, CP<B>], [C, CP<C>], [D, CP<D>], [E, CP<E>], [F, CP<F>]];
    children?: ReactNode;
  }): ReactElement;
  <A extends JEC<any>, B extends JEC<any>, C extends JEC<any>, D extends JEC<any>, E extends JEC<any>>(p: {
    providers: [[A, CP<A>], [B, CP<B>], [C, CP<C>], [D, CP<D>], [E, CP<E>]];
    children?: ReactNode;
  }): ReactElement;
  <A extends JEC<any>, B extends JEC<any>, C extends JEC<any>, D extends JEC<any>>(p: {
    providers: [[A, CP<A>], [B, CP<B>], [C, CP<C>], [D, CP<D>]];
    children?: ReactNode;
  }): ReactElement;
  <A extends JEC<any>, B extends JEC<any>, C extends JEC<any>>(p: {
    providers: [[A, CP<A>], [B, CP<B>], [C, CP<C>]];
    children?: ReactNode;
  }): ReactElement;
  <A extends JEC<any>, B extends JEC<any>>(p: {
    providers: [[A, CP<A>], [B, CP<B>]];
    children?: ReactNode;
  }): ReactElement;
  <A extends JEC<any>>(p: { providers: [[A, CP<A>]]; children?: ReactNode }): ReactElement;
  <T extends JEC<any>>(p: { providers: [T, CP<T>][]; children?: ReactNode }): ReactElement;
}
const Compose: ComposeFn = ({ providers, children }: { providers: [JEC<any>, any][]; children?: ReactNode }) =>
  providers.reduceRight((acc, [Wrapper, props]) => React.createElement(Wrapper, props, acc), children) as ReactElement;

@malininss
Copy link

Hey @samMeow! Could you please explain what the advantages of this approach are? Your code looks much complicated than my and has restriction - only 6 nested components. And what is the type ComponentsProps?

@samMeow
Copy link

samMeow commented Jun 24, 2024

Hey @samMeow! Could you please explain what the advantages of this approach are? Your code looks much complicated than my and has restriction - only 6 nested components. And what is the type ComponentsProps?

The advantage of above code is to allow Providers to have different type, and each items would check against itself for props, while using only one generic T, will limit to only one kind of Provider
https://codesandbox.io/p/sandbox/sharp-bohr-cwh4ff?file=%2Fsrc%2FApp.tsx%3A27%2C19

It actually support more than 6 nested components with last bit T type as fallback, but it will lose the ability to check type for each items. I use 6 here just good enough for most case, so feel free to add more.

ComponentsProps my bad typos, changed to ComponentProps,

// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L1653
type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> = T extends
        JSXElementConstructor<infer P> ? P
        : T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T]
        : {};

p.s. still waiting a better Utility Type for compose in typescript (ref: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/lodash/fp.d.ts#L304)

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