diff --git a/packages/effect-fc/src/ReactComponent.ts b/packages/effect-fc/src/ReactComponent.ts index 2d62f42..dd8185d 100644 --- a/packages/effect-fc/src/ReactComponent.ts +++ b/packages/effect-fc/src/ReactComponent.ts @@ -1,47 +1,71 @@ -import { Context, Effect, ExecutionStrategy, Exit, Function, Ref, Runtime, Scope, Tracer } from "effect" -import type { Mutable } from "effect/Types" +import { Context, Effect, ExecutionStrategy, Exit, Function, Pipeable, Ref, Runtime, Scope, String, Tracer, type Types, type Utils } from "effect" import * as React from "react" -import type * as ReactHook from "./ReactHook.js" -export interface ReactComponent { +export interface ReactComponent extends Pipeable.Pipeable { (props: P): Effect.Effect readonly displayName?: string + readonly options: Options } -export type Error = T extends ReactComponent ? E : never -export type Context = T extends ReactComponent ? R : never -export type Props = T extends ReactComponent ? P : never +export interface Options { + readonly finalizerExecutionMode: "sync" | "fork" + readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy +} + +export type Error = T extends ReactComponent ? E : never +export type Context = T extends ReactComponent ? R : never +export type Props = T extends ReactComponent ? P : never export const nonReactiveTags = [Tracer.ParentSpan] as const -export const withDisplayName: { - >(displayName: string): (self: C) => C - >(self: C, displayName: string): C -} = Function.dual(2, >( - self: C, - displayName: string, -): C => { - (self as Mutable).displayName = displayName - return self -}) +export interface MakeOptions { + readonly traced?: boolean + readonly finalizerExecutionMode?: "sync" | "fork" + readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy +} + +export const make = >, P>( + body: (props: P) => Generator, + options?: MakeOptions, +): ReactComponent< + [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never, + P +> => { + const displayName = !String.isEmpty(body.name) ? body.name : undefined + + const f = ((options?.traced ?? true) + ? displayName + ? Effect.fn(displayName)(body) + : Effect.fn(body) + : Effect.fnUntraced(body) + ) as ReactComponent + + f.pipe = function pipe() { return Pipeable.pipeArguments(this, arguments) }; + (f as Types.Mutable).displayName = displayName; + (f as Types.Mutable).options = { + finalizerExecutionStrategy: options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential, + finalizerExecutionMode: options?.finalizerExecutionMode ?? "sync", + } + + return f +} export const useFC: { ( - self: ReactComponent, - options?: ReactHook.ScopeOptions, + self: ReactComponent ): Effect.Effect, never, Exclude> } = Effect.fnUntraced(function* ( - self: ReactComponent, - options?: ReactHook.ScopeOptions, + self: ReactComponent ) { const runtimeRef = React.useRef>>(null!) runtimeRef.current = yield* Effect.runtime>() return React.useMemo(() => function ScopeProvider(props: P) { - const scope = useScope(runtimeRef.current, options) + const scope = useScope(runtimeRef.current, self.options) const FC = React.useMemo(() => { const f = (props: P) => Runtime.runSync(runtimeRef.current)( @@ -59,10 +83,10 @@ export const useFC: { const useScope = ( runtime: Runtime.Runtime, - options?: ReactHook.ScopeOptions, + options: Options, ) => { const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)( - Effect.all([Ref.make(true), makeScope(options)]) + Effect.all([Ref.make(true), Scope.make(options.finalizerExecutionStrategy)]) ), []) const [scope, setScope] = React.useState(initialScope) @@ -73,7 +97,7 @@ const useScope = ( () => closeScope(scope, runtime, options), ), - onFalse: () => makeScope(options).pipe( + onFalse: () => Scope.make(options.finalizerExecutionStrategy).pipe( Effect.tap(scope => Effect.sync(() => setScope(scope))), Effect.map(scope => () => closeScope(scope, runtime, options)), ), @@ -83,13 +107,12 @@ const useScope = ( return scope } -const makeScope = (options?: ReactHook.ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential) const closeScope = ( scope: Scope.CloseableScope, runtime: Runtime.Runtime, - options?: ReactHook.ScopeOptions, + options: Options, ) => { - switch (options?.finalizerExecutionMode ?? "sync") { + switch (options.finalizerExecutionMode) { case "sync": Runtime.runSync(runtime)(Scope.close(scope, Exit.void)) break @@ -104,27 +127,23 @@ export const use: { ( self: ReactComponent, fn: (Component: React.FC

) => React.ReactNode, - options?: ReactHook.ScopeOptions, ): Effect.Effect> -} = Effect.fnUntraced(function*(self, fn, options) { - return fn(yield* useFC(self, options)) +} = Effect.fnUntraced(function*(self, fn) { + return fn(yield* useFC(self)) }) export const withRuntime: { ( context: React.Context>, - options?: ReactHook.ScopeOptions, ): (self: ReactComponent) => React.FC

( self: ReactComponent, context: React.Context>, - options?: ReactHook.ScopeOptions, ): React.FC

-} = Function.dual(3, ( +} = Function.dual(2, ( self: ReactComponent, context: React.Context>, - options?: ReactHook.ScopeOptions, ): React.FC

=> function WithRuntime(props) { const runtime = React.useContext(context) - return React.createElement(Runtime.runSync(runtime)(useFC(self, options)), props) + return React.createElement(Runtime.runSync(runtime)(useFC(self)), props) }) diff --git a/packages/effect-fc/src/ReactHook.ts b/packages/effect-fc/src/ReactHook.ts index 141408f..bfdf70f 100644 --- a/packages/effect-fc/src/ReactHook.ts +++ b/packages/effect-fc/src/ReactHook.ts @@ -4,8 +4,8 @@ import { SetStateAction } from "./types/index.js" export interface ScopeOptions { - readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy readonly finalizerExecutionMode?: "sync" | "fork" + readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy } diff --git a/packages/example/src/routes/index.tsx b/packages/example/src/routes/index.tsx index 2d4878c..321a4a6 100644 --- a/packages/example/src/routes/index.tsx +++ b/packages/example/src/routes/index.tsx @@ -2,20 +2,17 @@ import { runtime } from "@/runtime" import { Todos } from "@/todo/Todos" import { TodosState } from "@/todo/TodosState.service" import { createFileRoute } from "@tanstack/react-router" -import { Effect, pipe } from "effect" +import { Effect } from "effect" import { ReactComponent, ReactHook } from "effect-fc" export const Route = createFileRoute("/")({ - component: pipe( - Effect.fn("Route")(function*() { - return yield* Effect.provide( - ReactComponent.use(Todos, Todos => ), - yield* ReactHook.useMemoLayer(TodosState.Default("todos")), - ) - }), - - ReactComponent.withDisplayName("Index"), - ReactComponent.withRuntime(runtime.context, { finalizerExecutionMode: "fork" }), + component: ReactComponent.make(function* Index() { + return yield* Effect.provide( + ReactComponent.use(Todos, Todos => ), + yield* ReactHook.useMemoLayer(TodosState.Default("todos")), + ) + }).pipe( + ReactComponent.withRuntime(runtime.context), ) }) diff --git a/packages/example/src/todo/Todo.tsx b/packages/example/src/todo/Todo.tsx index 413a0e0..4d71a54 100644 --- a/packages/example/src/todo/Todo.tsx +++ b/packages/example/src/todo/Todo.tsx @@ -1,7 +1,7 @@ import * as Domain from "@/domain" import { Button, Flex, TextArea } from "@radix-ui/themes" import { GetRandomValues, makeUuid4 } from "@typed/id" -import { Chunk, Effect, Match, Option, pipe, Ref, Runtime, SubscriptionRef } from "effect" +import { Chunk, Effect, Match, Option, Ref, Runtime, SubscriptionRef } from "effect" import { ReactComponent, ReactHook } from "effect-fc" import { SubscriptionSubRef } from "effect-fc/types" import * as React from "react" @@ -19,47 +19,43 @@ export type TodoProps = ( } ) -export const Todo = pipe( - Effect.fn(function*(props: TodoProps) { - const runtime = yield* Effect.runtime() - const state = yield* TodosState +export const Todo = ReactComponent.make(function* Todo(props: TodoProps) { + const runtime = yield* Effect.runtime() + const state = yield* TodosState - const ref = yield* ReactHook.useMemo(() => Match.value(props).pipe( - Match.tag("new", () => Effect.andThen(makeTodo, SubscriptionRef.make)), - Match.tag("edit", ({ ref }) => Effect.succeed(ref)), - Match.exhaustive, - ), [props._tag, props.ref]) + const ref = yield* ReactHook.useMemo(() => Match.value(props).pipe( + Match.tag("new", () => Effect.andThen(makeTodo, SubscriptionRef.make)), + Match.tag("edit", ({ ref }) => Effect.succeed(ref)), + Match.exhaustive, + ), [props._tag, props.ref]) - const contentRef = React.useMemo(() => SubscriptionSubRef.makeFromPath(ref, ["content"]), [ref]) - const [todo] = yield* ReactHook.useSubscribeRefs(ref) + const contentRef = React.useMemo(() => SubscriptionSubRef.makeFromPath(ref, ["content"]), [ref]) + const [todo] = yield* ReactHook.useSubscribeRefs(ref) - return ( - -