diff --git a/packages/effect-components/src/ReactComponent.ts b/packages/effect-components/src/ReactComponent.ts index d16c802..08115c0 100644 --- a/packages/effect-components/src/ReactComponent.ts +++ b/packages/effect-components/src/ReactComponent.ts @@ -1,6 +1,7 @@ -import { Context, Effect, Function, Runtime, Tracer } from "effect" +import { Context, Effect, Function, Runtime, Scope, Tracer } from "effect" import type { Mutable } from "effect/Types" import * as React from "react" +import * as ReactHook from "./ReactHook.js" export interface ReactComponent { @@ -22,23 +23,33 @@ export const withDisplayName: { }) export const useFC: { - (self: ReactComponent): Effect.Effect, never, R> -} = Effect.fnUntraced(function* ( + ( + self: ReactComponent + ): Effect.Effect, never, R> +} = Effect.fnUntraced(function* ( self: ReactComponent ) { const runtime = yield* Effect.runtime() - return React.useMemo(() => { - const FC = (props: P) => Runtime.runSync(runtime)(self(props)) - if (self.displayName) FC.displayName = self.displayName - return FC + return React.useMemo(() => function ScopeProvider(props: P) { + const scope = Runtime.runSync(runtime)(ReactHook.useScope()) + + const FC = React.useMemo(() => { + const f = (props: P) => Runtime.runSync(runtime)( + Effect.provideService(self(props), Scope.Scope, scope) + ) + if (self.displayName) f.displayName = self.displayName + return f + }, [scope]) + + return React.createElement(FC, props) }, Array.from( Context.omit(...nonReactiveTags)(runtime.context).unsafeMap.values() )) }) export const use: { - ( + ( self: ReactComponent, fn: (Component: React.FC

) => React.ReactNode, ): Effect.Effect @@ -47,8 +58,8 @@ export const use: { }) export const withRuntime: { - (context: React.Context>): (self: ReactComponent) => React.FC

- (self: ReactComponent, context: React.Context>): React.FC

+ (context: React.Context>): (self: ReactComponent) => React.FC

+ (self: ReactComponent, context: React.Context>): React.FC

} = Function.dual(2, ( self: ReactComponent, context: React.Context>, @@ -56,58 +67,3 @@ export const withRuntime: { const runtime = React.useContext(context) return React.createElement(Runtime.runSync(runtime)(useFC(self)), props) }) - -// export const useFC: { -// ( -// self: ReactComponent, -// options?: ReactHook.ScopeOptions, -// ): Effect.Effect, never, Exclude> -// } = Effect.fnUntraced(function* useFC( -// self: ReactComponent, -// options?: ReactHook.ScopeOptions, -// ) { -// const runtime = yield* Effect.runtime>() - -// return React.useCallback((props: P) => { -// const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)( -// Effect.all([Ref.make(true), makeScope(options)]) -// ), []) -// const [scope, setScope] = React.useState(initialScope) - -// React.useEffect(() => Runtime.runSync(runtime)( -// Effect.if(isInitialRun, { -// onTrue: () => Effect.as( -// Ref.set(isInitialRun, false), -// () => closeScope(scope, runtime, options), -// ), - -// onFalse: () => makeScope(options).pipe( -// Effect.tap(scope => Effect.sync(() => setScope(scope))), -// Effect.map(scope => () => closeScope(scope, runtime, options)), -// ), -// }) -// ), []) - -// return Runtime.runSync(runtime)( -// Effect.provideService(self(props), Scope.Scope, scope) -// ) -// }, Array.from( -// Context.omit(...nonReactiveTags)(runtime.context).unsafeMap.values() -// )) -// }) - -// const makeScope = (options?: ReactHook.ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential) -// const closeScope = ( -// scope: Scope.CloseableScope, -// runtime: Runtime.Runtime, -// options?: ReactHook.ScopeOptions, -// ) => { -// switch (options?.finalizerExecutionMode ?? "sync") { -// case "sync": -// Runtime.runSync(runtime)(Scope.close(scope, Exit.void)) -// break -// case "fork": -// Runtime.runFork(runtime)(Scope.close(scope, Exit.void)) -// break -// } -// } diff --git a/packages/effect-components/src/ReactHook.ts b/packages/effect-components/src/ReactHook.ts index a41d23a..c2cb50b 100644 --- a/packages/effect-components/src/ReactHook.ts +++ b/packages/effect-components/src/ReactHook.ts @@ -9,6 +9,50 @@ export interface ScopeOptions { } +export const useScope: { + (options?: ScopeOptions): Effect.Effect +} = Effect.fnUntraced(function* (options?: ScopeOptions) { + const runtime = yield* Effect.runtime() + + const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)( + Effect.all([Ref.make(true), makeScope(options)]) + ), []) + const [scope, setScope] = React.useState(initialScope) + + React.useEffect(() => Runtime.runSync(runtime)( + Effect.if(isInitialRun, { + onTrue: () => Effect.as( + Ref.set(isInitialRun, false), + () => closeScope(scope, runtime, options), + ), + + onFalse: () => makeScope(options).pipe( + Effect.tap(scope => Effect.sync(() => setScope(scope))), + Effect.map(scope => () => closeScope(scope, runtime, options)), + ), + }) + ), []) + + return scope +}) + +const makeScope = (options?: ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential) +const closeScope = ( + scope: Scope.CloseableScope, + runtime: Runtime.Runtime, + options?: ScopeOptions, +) => { + switch (options?.finalizerExecutionMode ?? "sync") { + case "sync": + Runtime.runSync(runtime)(Scope.close(scope, Exit.void)) + break + case "fork": + Runtime.runFork(runtime)(Scope.close(scope, Exit.void)) + break + } +} + + export const useMemo: { ( factory: () => Effect.Effect,