From f4864d50d57ae4d64026592a618a0665ce9885e5 Mon Sep 17 00:00:00 2001 From: yhuikzdtguioaert <174601769+yhuikzdtguioaert@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:44:43 +0300 Subject: [PATCH] feat(context): support provider props in MultiProvider --- .changeset/warm-context-props.md | 5 ++ packages/context/README.md | 18 ++++--- packages/context/src/index.ts | 72 ++++++++++++++++----------- packages/context/test/index.test.tsx | 31 ++++++++++++ packages/context/test/server.test.tsx | 34 +++++++++++++ 5 files changed, 126 insertions(+), 34 deletions(-) create mode 100644 .changeset/warm-context-props.md diff --git a/.changeset/warm-context-props.md b/.changeset/warm-context-props.md new file mode 100644 index 000000000..be5ade3db --- /dev/null +++ b/.changeset/warm-context-props.md @@ -0,0 +1,5 @@ +--- +"@solid-primitives/context": minor +--- + +Allow `MultiProvider` to pass arbitrary props to providers returned by `createContextProvider`. diff --git a/packages/context/README.md b/packages/context/README.md index 4b2b0eae7..9adcfeddc 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -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. @@ -102,11 +104,13 @@ import { MultiProvider } from "@solid-primitives/context"; - - - - - + + + + + + + ; @@ -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 diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index a38597c31..cfa2b0779 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -1,6 +1,7 @@ import { createContext, createComponent, + mergeProps, useContext, type JSX, type Context, @@ -8,12 +9,17 @@ import { } 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; export type ContextProvider = ( props: { children: JSX.Element } & T, ) => JSX.Element; +type CreatedContextProvider = ContextProvider & { + readonly [$PROVIDER_PROPS]: true; +}; /** * Create the Context Provider component and useContext function with types inferred from the factory function. @@ -39,26 +45,25 @@ export type ContextProvider = ( export function createContextProvider( factoryFn: (props: P) => T, defaults: T, -): [provider: ContextProvider

, useContext: () => T]; +): [provider: CreatedContextProvider

, useContext: () => T]; export function createContextProvider( factoryFn: (props: P) => T, -): [provider: ContextProvider

, useContext: () => T | undefined]; +): [provider: CreatedContextProvider

, useContext: () => T | undefined]; export function createContextProvider( factoryFn: (props: P) => T, defaults?: T, -): [provider: ContextProvider

, useContext: () => T | undefined] { +): [provider: CreatedContextProvider

, 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

; + Object.defineProperty(Provider, $PROVIDER_PROPS, { value: true }); + return [Provider, () => useContext(ctx)]; } /* @@ -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 @@ -88,21 +93,27 @@ Type validation of the `values` array thanks to the amazing @otonashixav (https: * // after * * * * ``` */ -export function MultiProvider(props: { - values: { - [K in keyof T]: - | readonly [ - Context | ContextProviderComponent, - [T[K]][T extends unknown ? 0 : never], - ] - | FlowComponent; - }; +type MultiProviderItem = T extends readonly [infer Provider, unknown] + ? Provider extends CreatedContextProvider + ? readonly [Provider, Omit] + : Provider extends Context + ? readonly [Provider, Value] + : Provider extends ContextProviderComponent + ? readonly [Provider, Value] + : never + : T extends FlowComponent + ? T + : never; + +export function MultiProvider(props: { + values: T & { [K in keyof T]: MultiProviderItem }; children: JSX.Element; }): JSX.Element { const { values } = props; @@ -111,15 +122,20 @@ export function MultiProvider(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); diff --git a/packages/context/test/index.test.tsx b/packages/context/test/index.test.tsx index a60a16639..430d1935e 100644 --- a/packages/context/test/index.test.tsx +++ b/packages/context/test/index.test.tsx @@ -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(() => { + + {untrack(() => { + capture = useTestContext().message; + return ""; + })} + ; + }); + + 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(() => { + + {untrack(() => { + capture = useContext(ObjectContext)?.message; + return ""; + })} + ; + }); + + expect(capture).toBe("Object context"); + }); }); diff --git a/packages/context/test/server.test.tsx b/packages/context/test/server.test.tsx index 8db05a5eb..eeb8e1f1a 100644 --- a/packages/context/test/server.test.tsx +++ b/packages/context/test/server.test.tsx @@ -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(() => ( + + {untrack(() => { + capture = useNamedContext(); + return ""; + })} + + )); + + 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(() => ( + + {untrack(() => { + capture = useContext(ObjectContext)?.message; + return ""; + })} + + )); + + expect(capture).toBe("Object context"); + }); });