diff --git a/packages/effect-components/src/ReactHook.ts b/packages/effect-components/src/ReactHook.ts index 7362ad0..befe806 100644 --- a/packages/effect-components/src/ReactHook.ts +++ b/packages/effect-components/src/ReactHook.ts @@ -1,4 +1,4 @@ -import { Effect, ExecutionStrategy, Runtime, Scope } from "effect" +import { Effect, ExecutionStrategy, Exit, pipe, Runtime, Scope, Stream, SubscriptionRef } from "effect" import * as React from "react" @@ -94,3 +94,58 @@ export const useLayoutEffect: { } }, deps) }) + +export const useFork: { + ( + effect: () => Effect.Effect, + deps?: React.DependencyList, + options?: Runtime.RunForkOptions & ScopeOptions, + ): Effect.Effect> +} = Effect.fnUntraced(function* ( + effect: () => Effect.Effect, + deps?: React.DependencyList, + options?: Runtime.RunForkOptions & ScopeOptions, +) { + const runtime = yield* Effect.runtime>() + + React.useEffect(() => { + const scope = Runtime.runSync(runtime)(options?.scope + ? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential) + : Scope.make(options?.finalizerExecutionStrategy) + ) + Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope }) + + return () => { + switch (options?.finalizerExecutionMode ?? "fork") { + case "sync": + Runtime.runSync(runtime)(Scope.close(scope, Exit.void)) + break + case "fork": + Runtime.runFork(runtime)(Scope.close(scope, Exit.void)) + break + } + } + }, deps) +}) + +export const useSubscribeRefs: { + []>( + ...refs: Refs + ): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success }> +} = Effect.fnUntraced(function* []>( + ...refs: Refs +) { + const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => + Effect.all(refs as readonly SubscriptionRef.SubscriptionRef[]) + )) + + yield* useFork(() => pipe( + refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)), + streams => Stream.zipLatestAll(...streams), + Stream.runForEach(v => + Effect.sync(() => setReactStateValue(v)) + ), + ), refs) + + return reactStateValue as any +}) diff --git a/packages/example/src/routes/effect-component-tests.tsx b/packages/example/src/routes/effect-component-tests.tsx index 51c5057..47d10b9 100644 --- a/packages/example/src/routes/effect-component-tests.tsx +++ b/packages/example/src/routes/effect-component-tests.tsx @@ -1,8 +1,7 @@ import { Box, TextField } from "@radix-ui/themes" import { createFileRoute } from "@tanstack/react-router" -import { Console, Effect, Layer, pipe, SubscriptionRef } from "effect" +import { Console, Effect, Layer, pipe, Ref, Runtime, SubscriptionRef } from "effect" import { ReactComponent, ReactHook, ReactManagedRuntime } from "effect-components" -import * as React from "react" const LogLive = Layer.scopedDiscard(Effect.acquireRelease( @@ -43,7 +42,10 @@ const MyRoute = pipe( const MyTestComponent = pipe( Effect.fn(function*() { - const [state, setState] = React.useState("value") + const runtime = yield* Effect.runtime() + + const testService = yield* TestService + const [value] = yield* ReactHook.useSubscribeRefs(testService.ref) yield* ReactHook.useEffect(() => Effect.andThen( Effect.addFinalizer(() => Console.log("MyTestComponent umounted")), @@ -53,8 +55,8 @@ const MyTestComponent = pipe( return <> setState(e.target.value)} + value={value} + onChange={e => Runtime.runSync(runtime)(Ref.set(testService.ref, e.target.value))} />