From d2e3c60e0d4efc6f51bf30a2ef5468e822888512 Mon Sep 17 00:00:00 2001 From: Fabian Hummel Date: Sat, 15 Feb 2025 16:37:51 +0100 Subject: [PATCH 01/10] Added context consumer --- packages/context/README.md | 36 +++++++++++++++++++++++++++ packages/context/src/index.ts | 27 ++++++++++++++++++++ packages/context/test/index.test.tsx | 25 ++++++++++++++++++- packages/context/test/server.test.tsx | 25 ++++++++++++++++++- 4 files changed, 111 insertions(+), 2 deletions(-) diff --git a/packages/context/README.md b/packages/context/README.md index 4b2b0eae7..c87a12591 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -12,6 +12,7 @@ Primitives simplifying the creation and use of SolidJS Context API. - [`createContextProvider`](#createcontextprovider) - Create the Context Provider component and useContext function with types inferred from the factory function. - [`MultiProvider`](#multiprovider) - A component that allows you to provide multiple contexts at once. +- [`ConsumeContext`](#consumecontext) - A component that allows you to consume contexts directly within JSX. ## Installation @@ -130,6 +131,41 @@ import { MultiProvider } from "@solid-primitives/context"; > **Warning** > Components and values passed to `MultiProvider` will be evaluated only once, so make sure that the structure is static. If is isn't, please use nested provider components instead. +## `ConsumeContext` + +Inspired by React's `Context.Consumer` component, `ConsumeContext` allows using contexts directly within JSX without the needing to extract the content JSX into a separate function. + +This is particularly useful when you want to use the context in the same JSX block where you're providing it and directly bind the context value to HTML. + +### How to use it + +`ConsumeContext` takes a `useFn` prop with the context's `use...` function and a `children` prop that will receive the context value. The use function may directly come from `createContextProvider`. + +```tsx +import { createContextProvider, ConsumeContext } from "@solid-primitives/context"; + +// Create a context provider +const [CounterProvider, useCounter] = createContextProvider(() => { + const [count, setCount] = createSignal(0); + const increment = () => setCount(count() + 1); + return { count, increment }; +}); + +// Provide it, consume it and use it in the same JSX block + + + {({ count, increment }) => ( +
+ + {count()} +
+ )} +
+
; +``` + +```tsx + ## Changelog See [CHANGELOG.md](./CHANGELOG.md) diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index 5af8c7a7b..95ffa4cad 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -119,3 +119,30 @@ export function MultiProvider(props }; return fn(0); } + +/** + * A component that allows you to consume a context without extracting the children into a separate function. + * This is particularly useful when the context needs to be used within the same JSX where it is provided. + * + * @param useFn A function that returns the context value. Preferably the `use...` function returned from `createContextProvider`. + * + * @example + * ```tsx + * // create the context + * const [CounterProvider, useCounter] // = createContextProvider(...) + * + * // use the context + * + * {({ count }) => ( + *
Count: {count()}
+ * )} + *
+ * ``` + */ +export function ConsumeContext(props: { + useFn: () => T | undefined, + children: (value: T | undefined) => JSX.Element +}): JSX.Element { + const context = props.useFn(); + return props.children(context); +} diff --git a/packages/context/test/index.test.tsx b/packages/context/test/index.test.tsx index db18cb2f4..7e09c437c 100644 --- a/packages/context/test/index.test.tsx +++ b/packages/context/test/index.test.tsx @@ -1,7 +1,7 @@ import { describe, test, expect } from "vitest"; import { createContext, createRoot, FlowComponent, JSX, untrack, useContext } from "solid-js"; import { render } from "solid-js/web"; -import { createContextProvider, MultiProvider } from "../src/index.js"; +import { ConsumeContext, createContextProvider, MultiProvider } from "../src/index.js"; type TestContextValue = { message: string; @@ -107,3 +107,26 @@ describe("MultiProvider", () => { expect(capture3).toBe(TEST_MESSAGE); }); }); + +describe("ConsumeContext", () => { + test("consumes a context", () => { + const Ctx = createContext("Hello"); + + function useCtx() { + return useContext(Ctx); + } + + let capture; + createRoot(() => { + + + {value => ( + capture = value + )} + + ; + }); + + expect(capture).toBe("World"); + }); +}); diff --git a/packages/context/test/server.test.tsx b/packages/context/test/server.test.tsx index 4e719e13a..5a3e8d7d2 100644 --- a/packages/context/test/server.test.tsx +++ b/packages/context/test/server.test.tsx @@ -1,7 +1,7 @@ import { describe, test, expect } from "vitest"; import { createContext, FlowComponent, JSX, untrack, useContext } from "solid-js"; import { renderToString } from "solid-js/web"; -import { createContextProvider, MultiProvider } from "../src/index.js"; +import { ConsumeContext, createContextProvider, MultiProvider } from "../src/index.js"; type TestContextValue = { message: string; @@ -59,3 +59,26 @@ describe("MultiProvider", () => { expect(capture3).toBe(TEST_MESSAGE); }); }); + +describe("ConsumeContext", () => { + test("consumes a context", () => { + const Ctx = createContext("Hello"); + + function useCtx() { + return useContext(Ctx); + } + + let capture; + renderToString(() => { + + + {value => ( + capture = value + )} + + ; + }); + + expect(capture).toBe("World"); + }); +}); From 7425c71bd767db40501605f7e42c82cb4b2a145f Mon Sep 17 00:00:00 2001 From: Fabian Hummel Date: Sun, 16 Feb 2025 12:23:19 +0100 Subject: [PATCH 02/10] Added raw context prop --- packages/context/README.md | 20 ++++++++++++++++++-- packages/context/src/index.ts | 25 +++++++++++++++++++++---- packages/context/test/index.test.tsx | 19 ++++++++++++++++++- packages/context/test/server.test.tsx | 19 ++++++++++++++++++- 4 files changed, 75 insertions(+), 8 deletions(-) diff --git a/packages/context/README.md b/packages/context/README.md index c87a12591..25769d44e 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -135,11 +135,13 @@ import { MultiProvider } from "@solid-primitives/context"; Inspired by React's `Context.Consumer` component, `ConsumeContext` allows using contexts directly within JSX without the needing to extract the content JSX into a separate function. -This is particularly useful when you want to use the context in the same JSX block where you're providing it and directly bind the context value to HTML. +This is particularly useful when you want to use the context in the same JSX block where you're providing it and directly bind the context value to the frontend. + +Note that this component solely serves as syntactic sugar and doesn't provide any additional functionality over inlining SolidJS's `useContext` hook within JSX. ### How to use it -`ConsumeContext` takes a `useFn` prop with the context's `use...` function and a `children` prop that will receive the context value. The use function may directly come from `createContextProvider`. +`ConsumeContext` takes a `useFn` prop with the context's `use...()` function and a `children` prop that will receive the context value. The use function may directly come from `createContextProvider()`. ```tsx import { createContextProvider, ConsumeContext } from "@solid-primitives/context"; @@ -164,7 +166,21 @@ const [CounterProvider, useCounter] = createContextProvider(() => { ; ``` +Alternatively, you may also pass the raw SolidJS context over `context` in case you have created a context using `createContext()`. + ```tsx +import { ConsumeContext } from "@solid-primitives/context"; + +// Create a context +const CounterContext = createContext(/*...*/); + +// Consume it using the raw context + + {({ count, increment }) => { + // ... + }} + +``` ## Changelog diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index 95ffa4cad..c9c9275ad 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -124,7 +124,19 @@ export function MultiProvider(props * A component that allows you to consume a context without extracting the children into a separate function. * This is particularly useful when the context needs to be used within the same JSX where it is provided. * - * @param useFn A function that returns the context value. Preferably the `use...` function returned from `createContextProvider`. + * The `ConsumeContext` component is equivalent to the following code and solely exists as syntactic sugar: + * + * ```tsx + * + * {untrack(() => { + * const context = useContext(counterContext); // or useCounter() + * return children(context); + * })} + * + * ``` + * + * @param useFn A function that returns the context value. Preferably the `use...()` function returned from `createContextProvider()`. + * @param context The context object itself returned by `createContext()`. If `useFn` is provided, this will be ignored. * * @example * ```tsx @@ -140,9 +152,14 @@ export function MultiProvider(props * ``` */ export function ConsumeContext(props: { - useFn: () => T | undefined, children: (value: T | undefined) => JSX.Element -}): JSX.Element { - const context = props.useFn(); +} & ({ + useFn: () => T | undefined, + context?: never; +} | { + useFn?: never; + context: Context; +})): JSX.Element { + const context = props.useFn ? props.useFn() : useContext(props.context); return props.children(context); } diff --git a/packages/context/test/index.test.tsx b/packages/context/test/index.test.tsx index 7e09c437c..db8ae3278 100644 --- a/packages/context/test/index.test.tsx +++ b/packages/context/test/index.test.tsx @@ -109,7 +109,7 @@ describe("MultiProvider", () => { }); describe("ConsumeContext", () => { - test("consumes a context", () => { + test("consumes a context via use-function", () => { const Ctx = createContext("Hello"); function useCtx() { @@ -129,4 +129,21 @@ describe("ConsumeContext", () => { expect(capture).toBe("World"); }); + + test("consumes a context via context object", () => { + const Ctx = createContext("Hello"); + + let capture; + createRoot(() => { + + + {value => ( + capture = value + )} + + ; + }); + + expect(capture).toBe("World"); + }); }); diff --git a/packages/context/test/server.test.tsx b/packages/context/test/server.test.tsx index 5a3e8d7d2..25d316ed1 100644 --- a/packages/context/test/server.test.tsx +++ b/packages/context/test/server.test.tsx @@ -61,7 +61,7 @@ describe("MultiProvider", () => { }); describe("ConsumeContext", () => { - test("consumes a context", () => { + test("consumes a context via use-function", () => { const Ctx = createContext("Hello"); function useCtx() { @@ -81,4 +81,21 @@ describe("ConsumeContext", () => { expect(capture).toBe("World"); }); + + test("consumes a context via context object", () => { + const Ctx = createContext("Hello"); + + let capture; + renderToString(() => { + + + {value => ( + capture = value + )} + + ; + }); + + expect(capture).toBe("World"); + }); }); From 55916fb3d679f50bd5ee4c9d2faaa9c649cc16fa Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Sun, 16 Feb 2025 17:36:16 +0100 Subject: [PATCH 03/10] context: Add changeset --- .changeset/strong-moose-attend.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/strong-moose-attend.md diff --git a/.changeset/strong-moose-attend.md b/.changeset/strong-moose-attend.md new file mode 100644 index 000000000..0de537f90 --- /dev/null +++ b/.changeset/strong-moose-attend.md @@ -0,0 +1,5 @@ +--- +"@solid-primitives/context": minor +--- + +Add `ConsumeContext` From ce9e9b6c87dcd424adae0505d1aa17b66c47d4b0 Mon Sep 17 00:00:00 2001 From: Fabian Hummel Date: Mon, 17 Feb 2025 13:33:57 +0100 Subject: [PATCH 04/10] Merged useFn and context --- packages/context/README.md | 14 ++++---- packages/context/src/index.ts | 52 ++++++++++++++++++--------- packages/context/test/index.test.tsx | 40 +++++++++------------ packages/context/test/server.test.tsx | 38 +++++++++----------- 4 files changed, 76 insertions(+), 68 deletions(-) diff --git a/packages/context/README.md b/packages/context/README.md index 25769d44e..0a719c8fd 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -141,7 +141,9 @@ Note that this component solely serves as syntactic sugar and doesn't provide an ### How to use it -`ConsumeContext` takes a `useFn` prop with the context's `use...()` function and a `children` prop that will receive the context value. The use function may directly come from `createContextProvider()`. +`ConsumeContext` takes a `use` prop that can be either one of the following: +* A `use...()` function returned by `createContextProvider` or a inline function that returns the context value like `() => useContext(MyContext)`. +* A `context` prop that takes a raw SolidJS context created by `createContext()`. ```tsx import { createContextProvider, ConsumeContext } from "@solid-primitives/context"; @@ -155,7 +157,7 @@ const [CounterProvider, useCounter] = createContextProvider(() => { // Provide it, consume it and use it in the same JSX block - + {({ count, increment }) => (
@@ -163,19 +165,19 @@ const [CounterProvider, useCounter] = createContextProvider(() => {
)}
-
; + ``` -Alternatively, you may also pass the raw SolidJS context over `context` in case you have created a context using `createContext()`. +With the raw SolidJS context returned by `createContext()`: ```tsx import { ConsumeContext } from "@solid-primitives/context"; // Create a context -const CounterContext = createContext(/*...*/); +const counterContext = createContext(/*...*/); // Consume it using the raw context - + {({ count, increment }) => { // ... }} diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index c9c9275ad..741300391 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -135,31 +135,49 @@ export function MultiProvider(props * * ``` * - * @param useFn A function that returns the context value. Preferably the `use...()` function returned from `createContextProvider()`. - * @param context The context object itself returned by `createContext()`. If `useFn` is provided, this will be ignored. + * @param use Either one of the following: + * - A function that returns the context value. Preferably the `use...()` function returned from `createContextProvider()`. + * - The context itself returned from `createContext()`. + * - A inline function that returns the context value. * * @example * ```tsx * // create the context * const [CounterProvider, useCounter] // = createContextProvider(...) * - * // use the context - * - * {({ count }) => ( - *
Count: {count()}
- * )} - *
+ * // provide and use the context + * + * + * {({ count }) => ( + *
Count: {count()}
+ * )} + *
+ *
+ * ``` + * + * ```tsx + * // create the context + * const counterContext = createContext({ count: 0 }); + * + * // provide and use the context + * + * + * {({ count }) => ( + *
Count: {count}
+ * )} + *
+ *
* ``` */ export function ConsumeContext(props: { - children: (value: T | undefined) => JSX.Element -} & ({ - useFn: () => T | undefined, - context?: never; -} | { - useFn?: never; - context: Context; -})): JSX.Element { - const context = props.useFn ? props.useFn() : useContext(props.context); + children: (value: T | undefined) => JSX.Element, + use: (() => T | undefined) | Context, +}): JSX.Element { + let context: T | undefined; + if (typeof props.use === "function") { + context = props.use(); + } else { + context = useContext(props.use); + } return props.children(context); } diff --git a/packages/context/test/index.test.tsx b/packages/context/test/index.test.tsx index db8ae3278..e9fd5a56d 100644 --- a/packages/context/test/index.test.tsx +++ b/packages/context/test/index.test.tsx @@ -109,41 +109,35 @@ describe("MultiProvider", () => { }); describe("ConsumeContext", () => { - test("consumes a context via use-function", () => { + test("consumes a context", () => { const Ctx = createContext("Hello"); + const useCtx = () => useContext(Ctx); - function useCtx() { - return useContext(Ctx); - } - - let capture; + let capture1; + let capture2; + let capture3; createRoot(() => { - + {value => ( - capture = value + capture1 = value )} - ; - }); - - expect(capture).toBe("World"); - }); - - test("consumes a context via context object", () => { - const Ctx = createContext("Hello"); - - let capture; - createRoot(() => { - - + {value => ( - capture = value + capture2 = value + )} + + useContext(Ctx)}> + {value => ( + capture3 = value )} ; }); - expect(capture).toBe("World"); + expect(capture1).toBe("World"); + expect(capture2).toBe("World"); + expect(capture3).toBe("World"); }); }); diff --git a/packages/context/test/server.test.tsx b/packages/context/test/server.test.tsx index 25d316ed1..b9133e9bb 100644 --- a/packages/context/test/server.test.tsx +++ b/packages/context/test/server.test.tsx @@ -63,39 +63,33 @@ describe("MultiProvider", () => { describe("ConsumeContext", () => { test("consumes a context via use-function", () => { const Ctx = createContext("Hello"); + const useCtx = () => useContext(Ctx); - function useCtx() { - return useContext(Ctx); - } - - let capture; + let capture1; + let capture2; + let capture3; renderToString(() => { - + {value => ( - capture = value + capture1 = value )} - ; - }); - - expect(capture).toBe("World"); - }); - - test("consumes a context via context object", () => { - const Ctx = createContext("Hello"); - - let capture; - renderToString(() => { - - + {value => ( - capture = value + capture2 = value + )} + + useContext(Ctx)}> + {value => ( + capture3 = value )} ; }); - expect(capture).toBe("World"); + expect(capture1).toBe("World"); + expect(capture2).toBe("World"); + expect(capture3).toBe("World"); }); }); From d00035625a016f44c6b8a5d26bfb1f6eecdd4a4a Mon Sep 17 00:00:00 2001 From: Fabian Hummel Date: Sun, 5 Jul 2026 16:28:58 +0200 Subject: [PATCH 05/10] Migration to v2 --- packages/context/src/index.ts | 39 ++++++------ packages/context/test/index.test.tsx | 93 +++++++++++++++++++--------- 2 files changed, 85 insertions(+), 47 deletions(-) diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index 186591167..34c80c283 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -238,10 +238,13 @@ export function MultiProvider(props * * ``` * - * @param use Either one of the following: - * - A function that returns the context value. Preferably the `use...()` function returned from `createContextProvider()`. - * - The context itself returned from `createContext()`. - * - A inline function that returns the context value. + * @param provider Either one of the following: + * - A function that returns the context value. Preferably the `use...()` function returned by `createContextProvider()`. + * - The context itself returned by `createContext()`. + * - An inline function that returns the context's value. + * + * Please note that this prop is not reactive. + * * * @example * ```tsx @@ -250,37 +253,37 @@ export function MultiProvider(props * * // provide and use the context * - * + * * {({ count }) => ( *
Count: {count()}
* )} - *
+ * *
* ``` * * ```tsx * // create the context - * const counterContext = createContext({ count: 0 }); + * const CounterContext = createContext({ count: 0 }); * * // provide and use the context - * - * + * + * * {({ count }) => ( *
Count: {count}
* )} - *
- *
+ * + * * ``` */ -export function ConsumeContext(props: { - children: (value: T | undefined) => JSX.Element, - use: (() => T | undefined) | Context, -}): JSX.Element { +export function ContextConsumer(props: { + children: (value: T | undefined) => Element, + provider: (() => T | undefined) | Context, +}): Element { let context: T | undefined; - if (typeof props.use === "function") { - context = props.use(); + if (props.provider.length === 0) { + context = props.provider(undefined!) as T | undefined; } else { - context = useContext(props.use); + context = useContext(props.provider as Context); } return props.children(context); } diff --git a/packages/context/test/index.test.tsx b/packages/context/test/index.test.tsx index a9b50b80d..77a7d43d3 100644 --- a/packages/context/test/index.test.tsx +++ b/packages/context/test/index.test.tsx @@ -6,6 +6,7 @@ import { createOptionalContextProvider, createLayeredContext, MultiProvider, + ContextConsumer, } from "../src/index.js"; type TestContextValue = { @@ -343,36 +344,70 @@ describe("MultiProvider", () => { }); describe("ConsumeContext", () => { - test("consumes a context", () => { - const Ctx = createContext("Hello"); - const useCtx = () => useContext(Ctx); + test("consumes a context directly", () => { + const Context = createContext("Default"); - let capture1; - let capture2; - let capture3; - createRoot(() => { - - - {value => ( - capture1 = value - )} - - - {value => ( - capture2 = value - )} - - useContext(Ctx)}> - {value => ( - capture3 = value - )} - - ; - }); - - expect(capture1).toBe("World"); - expect(capture2).toBe("World"); - expect(capture3).toBe("World"); + let capture; + const unmount = render( + () => ( + + + {value => ( + capture = value + )} + + + ), + document.createElement("div") + ); + + expect(capture).toBe("Actual"); + + unmount(); + }); + + test("consumes a context via global use-function", () => { + const Context = createContext("Default"); + + let capture; + const unmount = render( + () => ( + + useContext(Context)}> + {value => ( + capture = value + )} + + + ), + document.createElement("div") + ); + + expect(capture).toBe("Actual"); + + unmount(); + }); + + test("consumes a context via its use-function from `createContextProvider`", () => { + const [Provider, useContext] = createContextProvider( + (props: { value: string }) => props.value, + "Default"); + + let capture; + const unmount = render( + () => ( + + + {value => ( + capture = value + )} + + + ), + document.createElement("div") + ); + + expect(capture).toBe("Actual"); unmount(); }); From 0db07e84004132770ea12fff1996df0b128c2203 Mon Sep 17 00:00:00 2001 From: Fabian Hummel Date: Mon, 6 Jul 2026 15:05:15 +0200 Subject: [PATCH 06/10] Adjustments to Unit Tests and README.md --- .changeset/strong-moose-attend.md | 5 -- packages/context/README.md | 43 ++++++++-------- packages/context/test/index.test.tsx | 2 +- packages/context/test/server.test.tsx | 70 ++++++++++++++++++--------- 4 files changed, 68 insertions(+), 52 deletions(-) delete mode 100644 .changeset/strong-moose-attend.md diff --git a/.changeset/strong-moose-attend.md b/.changeset/strong-moose-attend.md deleted file mode 100644 index 0de537f90..000000000 --- a/.changeset/strong-moose-attend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@solid-primitives/context": minor ---- - -Add `ConsumeContext` diff --git a/packages/context/README.md b/packages/context/README.md index 3cc9883ae..0e03854b1 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -16,7 +16,7 @@ Primitives simplifying the creation and use of SolidJS Context API. - [`createOptionalContextProvider`](#createoptionalcontextprovider) - Like `createContextProvider`, but returns `undefined` instead of throwing if the context is missing. - [`createLayeredContext`](#createlayeredcontext) - Like `createContextProvider`, but each provider extends the parent context value rather than replacing it. - [`MultiProvider`](#multiprovider) - A component that allows you to provide multiple contexts at once. -- [`ConsumeContext`](#consumecontext) - A component that allows you to consume contexts directly within JSX. +- [`ContextConsumer`](#contextconsumer) - A component that allows you to consume contexts directly within JSX. ## Installation @@ -187,22 +187,22 @@ import { MultiProvider } from "@solid-primitives/context"; > **Warning** > Components and values passed to `MultiProvider` will be evaluated only once, so make sure that the structure is static. If it isn't, please use nested provider components instead. -## `ConsumeContext` +## `ContextConsumer` -Inspired by React's `Context.Consumer` component, `ConsumeContext` allows using contexts directly within JSX without the needing to extract the content JSX into a separate function. +Inspired by React's `Context.Consumer` component, `ContextConsumer` allows using contexts directly within JSX without the needing to extract the content JSX into a separate function. This is particularly useful when you want to use the context in the same JSX block where you're providing it and directly bind the context value to the frontend. -Note that this component solely serves as syntactic sugar and doesn't provide any additional functionality over inlining SolidJS's `useContext` hook within JSX. +Note that this component solely serves as syntactic sugar and doesn't provide any additional functionality over using `untrack` together with `useContext` in JSX. ### How to use it -`ConsumeContext` takes a `use` prop that can be either one of the following: -* A `use...()` function returned by `createContextProvider` or a inline function that returns the context value like `() => useContext(MyContext)`. -* A `context` prop that takes a raw SolidJS context created by `createContext()`. +`ContextConsumer` takes a `provider` prop that can be either one of the following: +* A `use...()` function returned by `createContextProvider` or an inline function that returns the context's value: `() => useContext(MyContext)`. +* A raw SolidJS context returned by `createContext()`. ```tsx -import { createContextProvider, ConsumeContext } from "@solid-primitives/context"; +import { createContextProvider, ContextConsumer } from "@solid-primitives/context"; // Create a context provider const [CounterProvider, useCounter] = createContextProvider(() => { @@ -211,33 +211,32 @@ const [CounterProvider, useCounter] = createContextProvider(() => { return { count, increment }; }); -// Provide it, consume it and use it in the same JSX block +// Provide and consume the context within same JSX block - + {({ count, increment }) => ( -
- - {count()} -
+ // ... )} -
+
``` With the raw SolidJS context returned by `createContext()`: ```tsx -import { ConsumeContext } from "@solid-primitives/context"; +import { ContextConsumer } from "@solid-primitives/context"; // Create a context -const counterContext = createContext(/*...*/); +const CounterContext = createContext(/*...*/); // Consume it using the raw context - - {({ count, increment }) => { - // ... - }} - + + + {({ count, increment }) => { + // ... + }} + + ``` ## Changelog diff --git a/packages/context/test/index.test.tsx b/packages/context/test/index.test.tsx index 77a7d43d3..b63beaa25 100644 --- a/packages/context/test/index.test.tsx +++ b/packages/context/test/index.test.tsx @@ -343,7 +343,7 @@ describe("MultiProvider", () => { }); }); -describe("ConsumeContext", () => { +describe("ContextConsumer", () => { test("consumes a context directly", () => { const Context = createContext("Default"); diff --git a/packages/context/test/server.test.tsx b/packages/context/test/server.test.tsx index 6d8dbc597..8be4dd8b4 100644 --- a/packages/context/test/server.test.tsx +++ b/packages/context/test/server.test.tsx @@ -1,7 +1,7 @@ import { describe, test, expect } from "vitest"; import { createContext, type FlowComponent, type Element, untrack, useContext } from "solid-js"; import { renderToString } from "@solidjs/web"; -import { createContextProvider, createLayeredContext, MultiProvider } from "../src/index.js"; +import { ContextConsumer, createContextProvider, createLayeredContext, MultiProvider } from "../src/index.js"; type TestContextValue = { message: string; @@ -85,36 +85,58 @@ describe("createLayeredContext (SSR)", () => { }); }); -describe("ConsumeContext", () => { - test("consumes a context via use-function", () => { - const Ctx = createContext("Hello"); - const useCtx = () => useContext(Ctx); - let capture1; - let capture2; - let capture3; - renderToString(() => { - - +describe("ContextConsumer (SSR)", () => { + test("consumes a context directly", () => { + const Context = createContext("Default"); + + let capture; + renderToString(() => ( + + {value => ( - capture1 = value + capture = value )} - - + + + )); + + expect(capture).toBe("Actual"); + }); + + test("consumes a context via global use-function", () => { + const Context = createContext("Default"); + + let capture; + renderToString(() => ( + + useContext(Context)}> {value => ( - capture2 = value + capture = value )} - - useContext(Ctx)}> + + + )); + + expect(capture).toBe("Actual"); + }); + + test("consumes a context via its use-function from `createContextProvider`", () => { + const [Provider, useContext] = createContextProvider( + (props: { value: string }) => props.value, + "Default"); + + let capture; + renderToString(() => ( + + {value => ( - capture3 = value + capture = value )} - - ; - }); + + + )); - expect(capture1).toBe("World"); - expect(capture2).toBe("World"); - expect(capture3).toBe("World"); + expect(capture).toBe("Actual"); }); }); From 69ad8c498d192891f7c4f5ace64a807f0646010d Mon Sep 17 00:00:00 2001 From: Fabian Hummel <98157550+FabianHummel@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:14:01 +0200 Subject: [PATCH 07/10] Fix documentation for context consumer component Updated documentation to correct the component name from 'ConsumeContext' to 'ContextConsumer'. --- packages/context/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index 34c80c283..24c4b0f21 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -227,7 +227,7 @@ export function MultiProvider(props * A component that allows you to consume a context without extracting the children into a separate function. * This is particularly useful when the context needs to be used within the same JSX where it is provided. * - * The `ConsumeContext` component is equivalent to the following code and solely exists as syntactic sugar: + * The `ContextConsumer` component is equivalent to the following code and solely exists as syntactic sugar: * * ```tsx * From dd431943c88955793aa91e300a7e19535594d545 Mon Sep 17 00:00:00 2001 From: Fabian Hummel Date: Mon, 6 Jul 2026 15:27:16 +0200 Subject: [PATCH 08/10] Revert remove changeset --- .changeset/strong-moose-attend.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/strong-moose-attend.md diff --git a/.changeset/strong-moose-attend.md b/.changeset/strong-moose-attend.md new file mode 100644 index 000000000..0de537f90 --- /dev/null +++ b/.changeset/strong-moose-attend.md @@ -0,0 +1,5 @@ +--- +"@solid-primitives/context": minor +--- + +Add `ConsumeContext` From edffd6116edbfac07214ea794ae58386f0f29811 Mon Sep 17 00:00:00 2001 From: Fabian Hummel Date: Tue, 7 Jul 2026 10:20:27 +0200 Subject: [PATCH 09/10] Added fallback prop and Unit Tests --- .changeset/strong-moose-attend.md | 2 +- packages/context/README.md | 30 +++++++--- packages/context/src/index.ts | 19 +++++-- packages/context/test/index.test.tsx | 80 +++++++++++++++++++++++++++ packages/context/test/server.test.tsx | 62 ++++++++++++++++++++- 5 files changed, 177 insertions(+), 16 deletions(-) diff --git a/.changeset/strong-moose-attend.md b/.changeset/strong-moose-attend.md index 0de537f90..b8b78b251 100644 --- a/.changeset/strong-moose-attend.md +++ b/.changeset/strong-moose-attend.md @@ -2,4 +2,4 @@ "@solid-primitives/context": minor --- -Add `ConsumeContext` +Add `ContextConsumer` diff --git a/packages/context/README.md b/packages/context/README.md index 0e03854b1..1d64834e6 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -201,6 +201,8 @@ Note that this component solely serves as syntactic sugar and doesn't provide an * A `use...()` function returned by `createContextProvider` or an inline function that returns the context's value: `() => useContext(MyContext)`. * A raw SolidJS context returned by `createContext()`. +In case the context is not provided, it renders the element specified in the `fallback` prop. If no fallback is provided, this component will throw an error. + ```tsx import { createContextProvider, ContextConsumer } from "@solid-primitives/context"; @@ -212,13 +214,22 @@ const [CounterProvider, useCounter] = createContextProvider(() => { }); // Provide and consume the context within same JSX block - - - {({ count, increment }) => ( - // ... - )} - - +return ( + + ( +
+

Fallback

+
+ )}> + {({ count, increment }) => ( +
+

Count: {count()}

+ +
+ )} +
+
+); ``` With the raw SolidJS context returned by `createContext()`: @@ -233,7 +244,10 @@ const CounterContext = createContext(/*...*/); {({ count, increment }) => { - // ... +
+

Count: {count()}

+ +
}}
diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index 24c4b0f21..c7e17666f 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -194,11 +194,11 @@ Type validation of the `values` array thanks to the amazing @otonashixav (https: export function MultiProvider(props: { values: { [K in keyof T]: - | readonly [ - Context | ContextProviderComponent, - [T[K]][T extends unknown ? 0 : never], - ] - | FlowComponent; + | readonly [ + Context | ContextProviderComponent, + [T[K]][T extends unknown ? 0 : never], + ] + | FlowComponent; }; children: Element; }): Element { @@ -276,7 +276,8 @@ export function MultiProvider(props * ``` */ export function ContextConsumer(props: { - children: (value: T | undefined) => Element, + children: (value: T) => Element, + fallback?: (() => Element) | Element, provider: (() => T | undefined) | Context, }): Element { let context: T | undefined; @@ -285,5 +286,11 @@ export function ContextConsumer(props: { } else { context = useContext(props.provider as Context); } + if (context === undefined) { + if (typeof props.fallback === "function") { + return props.fallback?.(); + } + return props.fallback; + } return props.children(context); } diff --git a/packages/context/test/index.test.tsx b/packages/context/test/index.test.tsx index b63beaa25..613aec457 100644 --- a/packages/context/test/index.test.tsx +++ b/packages/context/test/index.test.tsx @@ -411,4 +411,84 @@ describe("ContextConsumer", () => { unmount(); }); + + test("consumes a context directly without providing context (default value)", () => { + const Context = createContext("Default"); + + let capture; + const unmount = render( + () => ( + + {value => ( + capture = value + )} + + ), + document.createElement("div") + ); + + expect(capture).toBe("Default"); + + unmount(); + }); + + test("consumes a context via its use-function from `createContextProvider` without provider (default value)", () => { + const [, useContext] = createContextProvider( + (props: { value: string }) => props.value, + "Default" + ); + + let capture; + const unmount2 = render( + () => ( + + {value => ( + capture = value + )} + + ), + document.createElement("div") + ); + + expect(capture).toBe("Default"); + + unmount2(); + }); + + test("renders fallback prop when context is undefined", () => { + const [, useOptional] = createOptionalContextProvider((props: { value?: string }) => props.value); + const container = document.createElement("div"); + document.body.appendChild(container); + const unmount3 = render( + () => ( + "Fallback"}> + {value => ( + value as any + )} + + ), + container, + ); + + expect(container.innerHTML).toBe("Fallback"); + + unmount3(); + document.body.removeChild(container); + }); + + test("throws when provider absent and no fallback", () => { + const [, useRequired] = createContextProvider(() => ({ value: 1 })); + expect(() => + render( + () => ( + + {value => ( + value as any + )} + + ), + document.createElement("div") + ) + ).toThrow(); + }); }); diff --git a/packages/context/test/server.test.tsx b/packages/context/test/server.test.tsx index 8be4dd8b4..88bfe493b 100644 --- a/packages/context/test/server.test.tsx +++ b/packages/context/test/server.test.tsx @@ -1,7 +1,7 @@ import { describe, test, expect } from "vitest"; import { createContext, type FlowComponent, type Element, untrack, useContext } from "solid-js"; import { renderToString } from "@solidjs/web"; -import { ContextConsumer, createContextProvider, createLayeredContext, MultiProvider } from "../src/index.js"; +import { ContextConsumer, createContextProvider, createOptionalContextProvider, createLayeredContext, MultiProvider } from "../src/index.js"; type TestContextValue = { message: string; @@ -139,4 +139,64 @@ describe("ContextConsumer (SSR)", () => { expect(capture).toBe("Actual"); }); + + test("consumes a context directly without providing context (default value)", () => { + const Context = createContext("Default"); + + let capture; + renderToString(() => ( + + {value => ( + capture = value + )} + + )); + + expect(capture).toBe("Default"); + }); + + test("consumes a context via its use-function from `createContextProvider` without provider (default value)", () => { + const [, useContext] = createContextProvider( + (props: { value: string }) => props.value, + "Default"); + + let capture; + renderToString(() => ( + + {value => ( + capture = value + )} + + )); + + expect(capture).toBe("Default"); + }); + + test("renders fallback prop when context is undefined", () => { + const [, useOptional] = createOptionalContextProvider((props: { value?: string }) => props.value); + + let capture; + renderToString(() => ( + (capture = 'Fallback')}> + {value => ( + value as any + )} + + )); + + expect(capture).toBe("Fallback"); + }); + + test("throws when provider absent and no fallback", () => { + const [, useRequired] = createContextProvider(() => ({ value: 1 })); + expect(() => + renderToString(() => ( + + {value => ( + value as any + )} + + )) + ).toThrow(); + }); }); From 15c070e37454f1324c859dd69efa41bdda6bdefb Mon Sep 17 00:00:00 2001 From: Fabian Hummel Date: Tue, 7 Jul 2026 12:18:24 +0200 Subject: [PATCH 10/10] Updated documentation regarding fallback values. --- packages/context/README.md | 2 +- packages/context/src/index.ts | 26 ++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/context/README.md b/packages/context/README.md index 1d64834e6..61bf903dc 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -201,7 +201,7 @@ Note that this component solely serves as syntactic sugar and doesn't provide an * A `use...()` function returned by `createContextProvider` or an inline function that returns the context's value: `() => useContext(MyContext)`. * A raw SolidJS context returned by `createContext()`. -In case the context is not provided, it renders the element specified in the `fallback` prop. If no fallback is provided, this component will throw an error. +In case the context's value is `undefined` (e.g. by using `createOptionalContextProvider`), a fallback element specified in the `fallback` prop is rendered instead. If the context is not provided at all, the component will throw an error. ```tsx import { createContextProvider, ContextConsumer } from "@solid-primitives/context"; diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index c7e17666f..6d5bb0bff 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -245,11 +245,13 @@ export function MultiProvider(props * * Please note that this prop is not reactive. * + * @param fallback Optional fallback element to render when the context's value is `undefined`. Can be a function or an element. Note that the component still throws an error if the context is not provided, regardless of the fallback. * * @example + * with `createContextProvider`: * ```tsx * // create the context - * const [CounterProvider, useCounter] // = createContextProvider(...) + * const [CounterProvider, useCounter] = createContextProvider(...) * * // provide and use the context * @@ -261,9 +263,29 @@ export function MultiProvider(props * * ``` * + * @example + * with `createOptionalContextProvider` (and `fallback`): + * ```tsx + * // create the optional context + * const [OptionalCounterProvider, useOptionalCounter] = createOptionalContextProvider(...) + * + * // provide and use the context + * + * ( + *
No counter provided
+ * )}> + * {({ count }) => ( + *
Count: {count()}
+ * )} + *
+ *
+ * ``` + * + * @example + * with `createContext`: * ```tsx * // create the context - * const CounterContext = createContext({ count: 0 }); + * const CounterContext = createContext(...); * * // provide and use the context *