Skip to content
Open
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.
63 changes: 63 additions & 0 deletions packages/context/src/consume.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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;
}

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;
}

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>
);
}
56 changes: 2 additions & 54 deletions packages/context/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,15 @@ 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>;
export type ContextProvider<T extends ContextProviderProps> = (
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
* <CounterProvider initial={1}>
* <App/>
* </CounterProvider>
* // get the context
* const ctx = useCounter()
* ctx?.count() // => 1
* ```
*/
export function createContextProvider<T, P extends ContextProviderProps>(
factoryFn: (props: P) => T,
defaults: T,
Expand All @@ -61,39 +42,6 @@ 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: {
[K in keyof T]:
Expand Down
24 changes: 24 additions & 0 deletions packages/context/test/consume.test.tsx
Original file line number Diff line number Diff line change
@@ -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<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();
});
});
});
Loading