diff --git a/packages/reffuse/src/ReffuseContext.ts b/packages/reffuse/src/ReffuseContext.ts new file mode 100644 index 0000000..17b1588 --- /dev/null +++ b/packages/reffuse/src/ReffuseContext.ts @@ -0,0 +1,94 @@ +import { Array, Context, Effect, Layer, Runtime } from "effect" +import * as React from "react" +import * as ReffuseRuntime from "./ReffuseRuntime.js" + + +export class ReffuseContext { + readonly Context = React.createContext>(null!) + readonly Provider = makeProvider(this.Context) + readonly AsyncProvider = makeAsyncProvider(this.Context) + + + useContext(): Context.Context { + return React.useContext(this.Context) + } + + useLayer(): Layer.Layer { + const context = this.useContext() + return React.useMemo(() => Layer.effectContext(Effect.succeed(context)), [context]) + } +} + +export type R = T extends ReffuseContext ? R : never + + +export type ReactProvider = React.FC<{ + readonly layer: Layer.Layer + readonly children?: React.ReactNode +}> + +function makeProvider(Context: React.Context>): ReactProvider { + return function ReffuseContextReactProvider(props) { + const runtime = ReffuseRuntime.useRuntime() + + const value = React.useMemo(() => Effect.context().pipe( + Effect.provide(props.layer), + Runtime.runSync(runtime), + ), [props.layer, runtime]) + + return React.createElement(Context, { ...props, value }) + } +} + +export type AsyncReactProvider = React.FC<{ + readonly layer: Layer.Layer + readonly fallback?: React.ReactNode + readonly children?: React.ReactNode +}> + +function makeAsyncProvider(Context: React.Context>): AsyncReactProvider { + function Inner({ promise, children }: { + readonly promise: Promise> + readonly children?: React.ReactNode + }) { + const value = React.use(promise) + return React.createElement(Context, { value, children }) + } + + return function ReffuseContextAsyncReactProvider(props) { + const runtime = ReffuseRuntime.useRuntime() + + const promise = React.useMemo(() => Effect.context().pipe( + Effect.provide(props.layer), + Runtime.runPromise(runtime), + ), [props.layer, runtime]) + + const inner = React.createElement(Inner, { ...props, promise }) + return React.createElement(React.Suspense, { children: inner, fallback: props.fallback }) + } +} + + +export function make() { + return new ReffuseContext() +} + +export function useMergeAll>( + ...contexts: [...{ [K in keyof T]: ReffuseContext }] +): Context.Context { + const values = contexts.map(v => React.use(v.Context)) + return React.useMemo(() => Context.mergeAll(...values), values) +} + +export function useMergeAllLayers>( + ...contexts: [...{ [K in keyof T]: ReffuseContext }] +): Layer.Layer { + const values = contexts.map(v => React.use(v.Context)) + + return React.useMemo(() => Array.isNonEmptyArray(values) + ? Layer.mergeAll( + ...Array.map(values, context => Layer.effectContext(Effect.succeed(context))) + ) + : Layer.empty as Layer.Layer, + values) +}