Skip to content

Add ConsumeContext #756

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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/strong-moose-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solid-primitives/context": minor
---

Add `ConsumeContext`
54 changes: 54 additions & 0 deletions packages/context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
<CounterProvider>
<ConsumeContext use={useCounter}>
{({ count, increment }) => (
<div>
<button onClick={increment}>Increment</button>
<span>{count()}</span>
</div>
)}
</ConsumeContext>
</CounterProvider>
```

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
<ConsumeContext use={counterContext}>
{({ count, increment }) => {
// ...
}}
</ConsumeContext>
```

## Changelog

See [CHANGELOG.md](./CHANGELOG.md)
62 changes: 62 additions & 0 deletions packages/context/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,65 @@ export function MultiProvider<T extends readonly [unknown?, ...unknown[]]>(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
* <CounterProvider>
* {untrack(() => {
* const context = useContext(counterContext); // or useCounter()
* return children(context);
* })}
* </CounterProvider>
* ```
*
* @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
* <CounterProvider count={1}>
* <ConsumeContext use={useCounter}>
* {({ count }) => (
* <div>Count: {count()}</div>
* )}
* </ConsumeContext>
* </CounterProvider>
* ```
*
* ```tsx
* // create the context
* const counterContext = createContext({ count: 0 });
*
* // provide and use the context
* <counterContext.Provider value={{ count: 1 }}>
* <ConsumeContext use={counterContext}>
* {({ count }) => (
* <div>Count: {count}</div>
* )}
* </ConsumeContext>
* </counterContext.Provider>
* ```
*/
export function ConsumeContext<T>(props: {
children: (value: T | undefined) => JSX.Element,
use: (() => T | undefined) | Context<T>,
}): JSX.Element {
let context: T | undefined;
if (typeof props.use === "function") {
context = props.use();
} else {
context = useContext(props.use);
}
return props.children(context);
}
36 changes: 35 additions & 1 deletion packages/context/test/index.test.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -107,3 +107,37 @@ describe("MultiProvider", () => {
expect(capture3).toBe(TEST_MESSAGE);
});
});

describe("ConsumeContext", () => {
test("consumes a context", () => {
const Ctx = createContext<string>("Hello");
const useCtx = () => useContext(Ctx);

let capture1;
let capture2;
let capture3;
createRoot(() => {
<Ctx.Provider value="World">
<ConsumeContext use={Ctx}>
{value => (
capture1 = value
)}
</ConsumeContext>
<ConsumeContext use={useCtx}>
{value => (
capture2 = value
)}
</ConsumeContext>
<ConsumeContext use={() => useContext(Ctx)}>
{value => (
capture3 = value
)}
</ConsumeContext>
</Ctx.Provider>;
});

expect(capture1).toBe("World");
expect(capture2).toBe("World");
expect(capture3).toBe("World");
});
});
36 changes: 35 additions & 1 deletion packages/context/test/server.test.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -59,3 +59,37 @@ describe("MultiProvider", () => {
expect(capture3).toBe(TEST_MESSAGE);
});
});

describe("ConsumeContext", () => {
test("consumes a context via use-function", () => {
const Ctx = createContext<string>("Hello");
const useCtx = () => useContext(Ctx);

let capture1;
let capture2;
let capture3;
renderToString(() => {
<Ctx.Provider value="World">
<ConsumeContext use={Ctx}>
{value => (
capture1 = value
)}
</ConsumeContext>
<ConsumeContext use={useCtx}>
{value => (
capture2 = value
)}
</ConsumeContext>
<ConsumeContext use={() => useContext(Ctx)}>
{value => (
capture3 = value
)}
</ConsumeContext>
</Ctx.Provider>;
});

expect(capture1).toBe("World");
expect(capture2).toBe("World");
expect(capture3).toBe("World");
});
});