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` diff --git a/packages/context/README.md b/packages/context/README.md index 4b2b0eae7..0a719c8fd 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,59 @@ 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 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 `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"; + +// 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()} +
+ )} +
+
+``` + +With the raw SolidJS context returned by `createContext()`: + +```tsx +import { ConsumeContext } from "@solid-primitives/context"; + +// Create a context +const counterContext = createContext(/*...*/); + +// Consume it using the raw context + + {({ count, increment }) => { + // ... + }} + +``` + ## Changelog See [CHANGELOG.md](./CHANGELOG.md) diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index 5af8c7a7b..741300391 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -119,3 +119,65 @@ 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. + * + * 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 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(...) + * + * // 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, + 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 db18cb2f4..e9fd5a56d 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,37 @@ describe("MultiProvider", () => { expect(capture3).toBe(TEST_MESSAGE); }); }); + +describe("ConsumeContext", () => { + test("consumes a context", () => { + const Ctx = createContext("Hello"); + const useCtx = () => useContext(Ctx); + + 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"); + }); +}); diff --git a/packages/context/test/server.test.tsx b/packages/context/test/server.test.tsx index 4e719e13a..b9133e9bb 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,37 @@ describe("MultiProvider", () => { expect(capture3).toBe(TEST_MESSAGE); }); }); + +describe("ConsumeContext", () => { + test("consumes a context via use-function", () => { + const Ctx = createContext("Hello"); + const useCtx = () => useContext(Ctx); + + let capture1; + let capture2; + let capture3; + renderToString(() => { + + + {value => ( + capture1 = value + )} + + + {value => ( + capture2 = value + )} + + useContext(Ctx)}> + {value => ( + capture3 = value + )} + + ; + }); + + expect(capture1).toBe("World"); + expect(capture2).toBe("World"); + expect(capture3).toBe("World"); + }); +});