Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-context-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solid-primitives/context": minor
---

Allow `MultiProvider` to pass arbitrary props to providers returned by `createContextProvider`.
18 changes: 12 additions & 6 deletions packages/context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ It will work exactly like nesting multiple providers as separate components, but

### How to use it

`MultiProvider` takes only a single `values` with a key-value pair of the context and the value to provide.
`MultiProvider` takes a single `values` array. Contexts are paired with the value to provide.
Providers returned by `createContextProvider` are paired with their props instead, so providers with
arbitrary prop shapes can be composed without an extra wrapper component.

> **Note**
> Values list is evaluated in order, so the context values will be provided in the same way as if you were nesting the providers.
Expand All @@ -102,11 +104,13 @@ import { MultiProvider } from "@solid-primitives/context";
<FooContext.Provider value={"foo"}>
<BarContext.Provider value={"bar"}>
<BazContext.Provider value={"baz"}>
<MyCustomProviderComponent value={"hello-world"}>
<BoundContextProvider>
<App />
</BoundContextProvider>
</MyCustomProviderComponent>
<CounterProvider initial={1} label="primary">
<MyCustomProviderComponent value={"hello-world"}>
<BoundContextProvider>
<App />
</BoundContextProvider>
</MyCustomProviderComponent>
</CounterProvider>
</BazContext.Provider>
</BarContext.Provider>
</FooContext.Provider>;
Expand All @@ -117,6 +121,8 @@ import { MultiProvider } from "@solid-primitives/context";
[FooContext, "foo"],
[BarContext, "bar"],
[BazContext, "baz"],
// providers from createContextProvider receive the same props as when nested directly
[CounterProvider, { initial: 1, label: "primary" }],
// you can also provide a component, the value will be passed to a `value` prop
[MyCustomProviderComponent, "hello-world"],
// if you have a provider that doesn't accept a `value` prop, you can just pass a function
Expand Down
72 changes: 44 additions & 28 deletions packages/context/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import {
createContext,
createComponent,
mergeProps,
useContext,
type JSX,
type Context,
type FlowComponent,
} from "solid-js";
import type { ContextProviderComponent } from "../node_modules/solid-js/types/reactive/signal.js";

const $PROVIDER_PROPS = Symbol("provider-props");

export type ContextProviderProps = {
children?: JSX.Element;
} & Record<string, unknown>;
export type ContextProvider<T extends ContextProviderProps> = (
props: { children: JSX.Element } & T,
) => JSX.Element;
type CreatedContextProvider<T extends ContextProviderProps> = ContextProvider<T> & {
readonly [$PROVIDER_PROPS]: true;
};

/**
* Create the Context Provider component and useContext function with types inferred from the factory function.
Expand All @@ -39,26 +45,25 @@ export type ContextProvider<T extends ContextProviderProps> = (
export function createContextProvider<T, P extends ContextProviderProps>(
factoryFn: (props: P) => T,
defaults: T,
): [provider: ContextProvider<P>, useContext: () => T];
): [provider: CreatedContextProvider<P>, useContext: () => T];
export function createContextProvider<T, P extends ContextProviderProps>(
factoryFn: (props: P) => T,
): [provider: ContextProvider<P>, useContext: () => T | undefined];
): [provider: CreatedContextProvider<P>, useContext: () => T | undefined];
export function createContextProvider<T, P extends ContextProviderProps>(
factoryFn: (props: P) => T,
defaults?: T,
): [provider: ContextProvider<P>, useContext: () => T | undefined] {
): [provider: CreatedContextProvider<P>, useContext: () => T | undefined] {
const ctx = createContext(defaults);
return [
props => {
return createComponent(ctx.Provider, {
value: factoryFn(props),
get children() {
return props.children;
},
});
},
() => useContext(ctx),
];
const Provider = (props => {
return createComponent(ctx.Provider, {
value: factoryFn(props),
get children() {
return props.children;
},
});
}) as CreatedContextProvider<P>;
Object.defineProperty(Provider, $PROVIDER_PROPS, { value: true });
return [Provider, () => useContext(ctx)];
}

/*
Expand All @@ -74,7 +79,7 @@ Type validation of the `values` array thanks to the amazing @otonashixav (https:
/**
* A component that allows you to provide multiple contexts at once. It will work exactly like nesting multiple providers as separate components, but it will save you from the nesting.
*
* @param values Array of tuples of `[ContextProviderComponent, value]` or `[Context, value]` or bound `ContextProviderComponent` (that doesn't take a `value` property).
* @param values Array of context/value tuples, `createContextProvider` provider/props tuples, or bound provider components.
*
* @example
* ```tsx
Expand All @@ -88,21 +93,27 @@ Type validation of the `values` array thanks to the amazing @otonashixav (https:
* // after
* <MultiProvider values={[
* [CounterCtx.Provider, 1],
* [NameCtx.Provider, "John"]
* [NameCtx.Provider, "John"],
* [CounterProvider, { initial: 1 }]
* ]}>
* <App/>
* </MultiProvider>
* ```
*/
export function MultiProvider<T extends readonly [unknown?, ...unknown[]]>(props: {
values: {
[K in keyof T]:
| readonly [
Context<T[K]> | ContextProviderComponent<T[K]>,
[T[K]][T extends unknown ? 0 : never],
]
| FlowComponent;
};
type MultiProviderItem<T> = T extends readonly [infer Provider, unknown]
? Provider extends CreatedContextProvider<infer ProviderProps>
? readonly [Provider, Omit<ProviderProps, "children">]
: Provider extends Context<infer Value>
? readonly [Provider, Value]
: Provider extends ContextProviderComponent<infer Value>
? readonly [Provider, Value]
: never
: T extends FlowComponent
? T
: never;

export function MultiProvider<const T extends readonly unknown[]>(props: {
values: T & { [K in keyof T]: MultiProviderItem<T[K]> };
children: JSX.Element;
}): JSX.Element {
const { values } = props;
Expand All @@ -111,15 +122,20 @@ export function MultiProvider<T extends readonly [unknown?, ...unknown[]]>(props

if (!item) return props.children;

const ctxProps: { value?: any; children: JSX.Element } = {
let ctxProps: { value?: any; children: JSX.Element } = {
get children() {
return fn(i + 1);
},
};
if (Array.isArray(item)) {
ctxProps.value = item[1];
const value = item[1];
item = item[0];
if (typeof item !== "function") item = item.Provider;
if (typeof item === "function" && $PROVIDER_PROPS in item) {
ctxProps = mergeProps(value, ctxProps);
} else {
ctxProps.value = value;
if (typeof item !== "function") item = item.Provider;
}
}

return createComponent(item, ctxProps);
Expand Down
31 changes: 31 additions & 0 deletions packages/context/test/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,35 @@ describe("MultiProvider", () => {
expect(capture2).toBe("World");
expect(capture3).toBe(TEST_MESSAGE);
});

test("passes arbitrary props to context providers", () => {
let capture;

createRoot(() => {
<MultiProvider values={[[TestProvider, { text: "Provided through MultiProvider" }]]}>
{untrack(() => {
capture = useTestContext().message;
return "";
})}
</MultiProvider>;
});

expect(capture).toBe("Provided through MultiProvider");
});

test("keeps object values wrapped in the value prop for Solid context providers", () => {
const ObjectContext = createContext<{ message: string }>();
let capture;

createRoot(() => {
<MultiProvider values={[[ObjectContext.Provider, { message: "Object context" }]]}>
{untrack(() => {
capture = useContext(ObjectContext)?.message;
return "";
})}
</MultiProvider>;
});

expect(capture).toBe("Object context");
});
});
34 changes: 34 additions & 0 deletions packages/context/test/server.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,38 @@ describe("MultiProvider", () => {
expect(capture2).toBe("World");
expect(capture3).toBe(TEST_MESSAGE);
});

test("passes arbitrary props to context providers", () => {
const [NamedProvider, useNamedContext] = createContextProvider(
(props: { name: string; children: JSX.Element }) => props.name,
);
let capture;

renderToString(() => (
<MultiProvider values={[[NamedProvider, { name: "Provided through MultiProvider" }]]}>
{untrack(() => {
capture = useNamedContext();
return "";
})}
</MultiProvider>
));

expect(capture).toBe("Provided through MultiProvider");
});

test("keeps object values wrapped in the value prop for Solid context providers", () => {
const ObjectContext = createContext<{ message: string }>();
let capture;

renderToString(() => (
<MultiProvider values={[[ObjectContext.Provider, { message: "Object context" }]]}>
{untrack(() => {
capture = useContext(ObjectContext)?.message;
return "";
})}
</MultiProvider>
));

expect(capture).toBe("Object context");
});
});