Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/context-consume.md
Original file line number Diff line number Diff line change
@@ -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.
86 changes: 86 additions & 0 deletions packages/context/src/consume.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
useContext,
createMemo,
type Context,
type JSX,
type Accessor,
} from "solid-js";

export type ContextSource<T> = Context<T> | (() => T | undefined) | Accessor<T>;

export type ResolveContextValue<S> = S extends Context<infer T>
? T
: S extends () => infer T
? T
: never;

export type ResolveContextValues<T extends readonly unknown[]> = {
[K in keyof T]: ResolveContextValue<T[K]>;
};

export interface ConsumeContextPropsSingle<T> {
use: ContextSource<T>;
children: (value: T) => JSX.Element;
}

export interface ConsumeContextPropsMultiple<T extends readonly unknown[]> {
use: { [K in keyof T]: ContextSource<T[K]> };
children: (values: ResolveContextValues<T>) => JSX.Element;
}

function resolveContext<T>(source: ContextSource<T>): T {
if (typeof source === "function") {
return source() as T;
}
return useContext(source) as T;
}

/**
* A reactive JSX component that allows consuming one or multiple contexts directly in JSX without extracting child components.
* Wrapped in a reactive memo to ensure fine-grained re-renders when context values mutate.
*
* @example
* ```tsx
* // Single context
* <ConsumeContext use={useTheme}>
* {(theme) => <div class={theme()}>{...}</div>}
* </ConsumeContext>
*
* // Multi context
* <ConsumeContext use={[useTheme, useAuth]}>
* {([theme, auth]) => <div>{auth.user.name} ({theme})</div>}
* </ConsumeContext>
* ```
*/
export function ConsumeContext<T>(props: ConsumeContextPropsSingle<T>): JSX.Element;
export function ConsumeContext<T extends readonly unknown[]>(
props: ConsumeContextPropsMultiple<T>,
): 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;
}

/**
* Creates a typed JSX consumer component for a given useContext function or Context object.
*
* @param useContextFn The useContext hook function or raw Context object.
* @returns A JSX component that consumes the context directly.
*/
export function createContextConsumer<T>(
useContextFn: ContextSource<T>,
): (props: { children: (value: T) => JSX.Element }) => JSX.Element {
return (props: { children: (value: T) => JSX.Element }) => (
<ConsumeContext use={useContextFn}>{props.children}</ConsumeContext>
);
}
30 changes: 2 additions & 28 deletions packages/context/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
Expand Down Expand Up @@ -61,38 +63,10 @@ export function createContextProvider<T, P extends ContextProviderProps>(
];
}

/*

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
* <CounterCtx.Provider value={1}>
* <NameCtx.Provider value="John">
* <App/>
* </NameCtx.Provider>
* </CounterCtx.Provider>
*
* // after
* <MultiProvider values={[
* [CounterCtx.Provider, 1],
* [NameCtx.Provider, "John"]
* ]}>
* <App/>
* </MultiProvider>
* ```
*/
export function MultiProvider<T extends readonly [unknown?, ...unknown[]]>(props: {
values: {
Expand Down
50 changes: 50 additions & 0 deletions packages/context/test/consume.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, test, expect } from "vitest";
import { createContext, createRoot } from "solid-js";
import { ConsumeContext, createContextConsumer } from "../src/index";

describe("ConsumeContext", () => {
test("consumes single context", () => {
const Ctx = createContext<string>("default");

createRoot(dispose => {
let captured = "";
<Ctx.Provider value="hello">
<ConsumeContext use={Ctx}>
{val => {
captured = val;
return <div>{val}</div>;
}}
</ConsumeContext>
</Ctx.Provider>;

expect(captured).toBe("hello");
dispose();
});
});

test("consumes multiple contexts simultaneously", () => {
const CtxA = createContext<string>("A");
const CtxB = createContext<number>(42);

createRoot(dispose => {
let capturedA = "";
let capturedB = 0;

<CtxA.Provider value="Alpha">
<CtxB.Provider value={100}>
<ConsumeContext use={[CtxA, CtxB]}>
{([a, b]) => {
capturedA = a;
capturedB = b;
return <div>{a} {b}</div>;
}}
</ConsumeContext>
</CtxB.Provider>
</CtxA.Provider>;

expect(capturedA).toBe("Alpha");
expect(capturedB).toBe(100);
dispose();
});
});
});