From 98da84e6c6d6adeb8dd906a3c6b1557e2160c5ce Mon Sep 17 00:00:00 2001 From: deny-dz Date: Sat, 22 Aug 2026 22:56:23 +0800 Subject: [PATCH] feat(context): add reactive ConsumeContext and createContextConsumer --- .changeset/context-consume.md | 5 ++ packages/context/src/consume.tsx | 63 ++++++++++++++++++++++++++ packages/context/src/index.ts | 56 +---------------------- packages/context/test/consume.test.tsx | 24 ++++++++++ 4 files changed, 94 insertions(+), 54 deletions(-) create mode 100644 .changeset/context-consume.md create mode 100644 packages/context/src/consume.tsx create mode 100644 packages/context/test/consume.test.tsx diff --git a/.changeset/context-consume.md b/.changeset/context-consume.md new file mode 100644 index 000000000..7760600bb --- /dev/null +++ b/.changeset/context-consume.md @@ -0,0 +1,5 @@ +--- +"@solid-primitives/context": minor +--- + +Add `ConsumeContext` component for reactive single and tuple-based multi-context consumption directly in JSX, and `createContextConsumer` helper. diff --git a/packages/context/src/consume.tsx b/packages/context/src/consume.tsx new file mode 100644 index 000000000..682e8f48f --- /dev/null +++ b/packages/context/src/consume.tsx @@ -0,0 +1,63 @@ +import { + useContext, + createMemo, + type Context, + type JSX, + type Accessor, +} from "solid-js"; + +export type ContextSource = Context | (() => T | undefined) | Accessor; + +export type ResolveContextValue = S extends Context + ? T + : S extends () => infer T + ? T + : never; + +export type ResolveContextValues = { + [K in keyof T]: ResolveContextValue; +}; + +export interface ConsumeContextPropsSingle { + use: ContextSource; + children: (value: T) => JSX.Element; +} + +export interface ConsumeContextPropsMultiple { + use: { [K in keyof T]: ContextSource }; + children: (values: ResolveContextValues) => JSX.Element; +} + +function resolveContext(source: ContextSource): T { + if (typeof source === "function") { + return source() as T; + } + return useContext(source) as T; +} + +export function ConsumeContext(props: ConsumeContextPropsSingle): JSX.Element; +export function ConsumeContext( + props: ConsumeContextPropsMultiple, +): JSX.Element; +export function ConsumeContext(props: { + use: any; + children: (val: any) => JSX.Element; +}): JSX.Element { + const resolved = createMemo(() => { + const { use } = props; + if (Array.isArray(use)) { + return use.map(resolveContext); + } + return resolveContext(use); + }); + + return createMemo(() => props.children(resolved())) as unknown as JSX.Element; +} + +export function createContextConsumer( + useContextFn: ContextSource, +): (props: { children: (value: T) => JSX.Element }) => JSX.Element { + return (props: { children: (value: T) => JSX.Element }) => ( + {props.children} + ); +} diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index a38597c31..6830d312a 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -8,6 +8,8 @@ import { } from "solid-js"; import type { ContextProviderComponent } from "../node_modules/solid-js/types/reactive/signal.js"; +export * from "./consume.jsx"; + export type ContextProviderProps = { children?: JSX.Element; } & Record; @@ -15,27 +17,6 @@ export type ContextProvider = ( props: { children: JSX.Element } & T, ) => JSX.Element; -/** - * Create the Context Provider component and useContext function with types inferred from the factory function. - * @param factoryFn Factory function will run when the provider component in executed. It takes the provider component `props` as it's argument, and what it returns will be available in the contexts for all the underlying components. - * @param defaults fallback returned from useContext function if the context wasn't provided - * @returns tuple of `[provider component, useContext function]` - * @example - * ```tsx - * const [CounterProvider, useCounter] = createContextProvider((props: { initial: number }) => { - * const [count, setCount] = createSignal(props.initial); - * const increment = () => setCount(count() + 1) - * return { count, increment }; - * }); - * // Provide the context - * - * - * - * // get the context - * const ctx = useCounter() - * ctx?.count() // => 1 - * ``` - */ export function createContextProvider( factoryFn: (props: P) => T, defaults: T, @@ -61,39 +42,6 @@ export function createContextProvider( ]; } -/* - -MultiProvider inspired by the preact-multi-provider package from Marvin Hagemeister -See https://github.com/marvinhagemeister/preact-multi-provider - - -Type validation of the `values` array thanks to the amazing @otonashixav (https://github.com/otonashixav) - -*/ - -/** - * 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). - * - * @example - * ```tsx - * // before - * - * - * - * - * - * - * // after - * - * - * - * ``` - */ export function MultiProvider(props: { values: { [K in keyof T]: diff --git a/packages/context/test/consume.test.tsx b/packages/context/test/consume.test.tsx new file mode 100644 index 000000000..2bc3e659a --- /dev/null +++ b/packages/context/test/consume.test.tsx @@ -0,0 +1,24 @@ +import { describe, test, expect } from "vitest"; +import { createContext, createRoot } from "solid-js"; +import { ConsumeContext } from "../src/index"; + +describe("ConsumeContext", () => { + test("consumes single context", () => { + const Ctx = createContext("default"); + + createRoot(dispose => { + let captured = ""; + + + {val => { + captured = val; + return
{val}
; + }} +
+
; + + expect(captured).toBe("hello"); + dispose(); + }); + }); +});