From d2e3c60e0d4efc6f51bf30a2ef5468e822888512 Mon Sep 17 00:00:00 2001 From: Fabian Hummel Date: Sat, 15 Feb 2025 16:37:51 +0100 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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"); }); });