From 78a3735038397d384d9113923d9bba3b61c3e5c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 30 Jun 2025 21:44:29 +0200 Subject: [PATCH] Refactoring --- .../effect-components/src/ReactComponent.ts | 41 +++++++---- packages/effect-components/src/ReactHook.ts | 24 +++---- .../src/ReactManagedRuntime.ts | 29 ++++++++ packages/effect-components/src/index.ts | 1 + .../src/routes/effect-component-tests.tsx | 70 +++++++++++-------- 5 files changed, 112 insertions(+), 53 deletions(-) create mode 100644 packages/effect-components/src/ReactManagedRuntime.ts diff --git a/packages/effect-components/src/ReactComponent.ts b/packages/effect-components/src/ReactComponent.ts index e20f4a0..d16c802 100644 --- a/packages/effect-components/src/ReactComponent.ts +++ b/packages/effect-components/src/ReactComponent.ts @@ -1,23 +1,30 @@ -import { Context, Effect, Runtime, Tracer } from "effect" +import { Context, Effect, Function, Runtime, Tracer } from "effect" +import type { Mutable } from "effect/Types" import * as React from "react" -import * as ReactHook from "./ReactHook.js" -export interface ReactComponent { +export interface ReactComponent { (props: P): Effect.Effect readonly displayName?: string } 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 const useFC: { - ( - self: ReactComponent, - options?: ReactHook.ScopeOptions, - ): Effect.Effect, never, R> -} = Effect.fnUntraced(function* useFC( - self: ReactComponent + (self: ReactComponent): Effect.Effect, never, R> +} = Effect.fnUntraced(function* ( + self: ReactComponent ) { const runtime = yield* Effect.runtime() @@ -31,14 +38,24 @@ export const useFC: { }) export const use: { - ( - self: ReactComponent, + ( + self: ReactComponent, fn: (Component: React.FC

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

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

+} = Function.dual(2, ( + self: ReactComponent, + context: React.Context>, +): React.FC

=> function WithRuntime(props) { + const runtime = React.useContext(context) + return React.createElement(Runtime.runSync(runtime)(useFC(self)), props) +}) // export const useFC: { // ( diff --git a/packages/effect-components/src/ReactHook.ts b/packages/effect-components/src/ReactHook.ts index 960bc1f..7362ad0 100644 --- a/packages/effect-components/src/ReactHook.ts +++ b/packages/effect-components/src/ReactHook.ts @@ -13,7 +13,7 @@ export const useMemo: { factory: () => Effect.Effect, deps: React.DependencyList, ): Effect.Effect -} = Effect.fnUntraced(function* useMemo( +} = Effect.fnUntraced(function* ( factory: () => Effect.Effect, deps: React.DependencyList, ) { @@ -23,7 +23,7 @@ export const useMemo: { export const useOnce: { (factory: () => Effect.Effect): Effect.Effect -} = Effect.fnUntraced(function* useOnce( +} = Effect.fnUntraced(function* ( factory: () => Effect.Effect ) { return yield* useMemo(factory, []) @@ -31,16 +31,16 @@ export const useOnce: { export const useEffect: { ( - effect: () => Effect.Effect, + effect: () => Effect.Effect, deps?: React.DependencyList, options?: ScopeOptions, - ): Effect.Effect -} = Effect.fnUntraced(function* useEffect( - effect: () => Effect.Effect, + ): Effect.Effect> +} = Effect.fnUntraced(function* ( + effect: () => Effect.Effect, deps?: React.DependencyList, options?: ScopeOptions, ) { - const runtime = yield* Effect.runtime() + const runtime = yield* Effect.runtime>() React.useEffect(() => { const { scope, exit } = Effect.Do.pipe( @@ -64,16 +64,16 @@ export const useEffect: { export const useLayoutEffect: { ( - effect: () => Effect.Effect, + effect: () => Effect.Effect, deps?: React.DependencyList, options?: ScopeOptions, - ): Effect.Effect -} = Effect.fnUntraced(function* useLayoutEffect( - effect: () => Effect.Effect, + ): Effect.Effect> +} = Effect.fnUntraced(function* ( + effect: () => Effect.Effect, deps?: React.DependencyList, options?: ScopeOptions, ) { - const runtime = yield* Effect.runtime() + const runtime = yield* Effect.runtime>() React.useLayoutEffect(() => { const { scope, exit } = Effect.Do.pipe( diff --git a/packages/effect-components/src/ReactManagedRuntime.ts b/packages/effect-components/src/ReactManagedRuntime.ts new file mode 100644 index 0000000..d964e94 --- /dev/null +++ b/packages/effect-components/src/ReactManagedRuntime.ts @@ -0,0 +1,29 @@ +import { Effect, type Layer, ManagedRuntime, Runtime } from "effect" +import * as React from "react" + + +export interface ReactManagedRuntime { + readonly runtime: ManagedRuntime.ManagedRuntime + readonly context: React.Context> +} + +export const make = ( + layer: Layer.Layer, + memoMap?: Layer.MemoMap, +): ReactManagedRuntime => ({ + runtime: ManagedRuntime.make(layer, memoMap), + context: React.createContext>(null!), +}) + +export interface SyncProviderProps { + readonly runtime: ReactManagedRuntime + readonly children?: React.ReactNode +} + +export const SyncProvider = ( + props: SyncProviderProps +): React.ReactNode => React.createElement(props.runtime.context, { + value: React.useMemo(() => Effect.runSync(props.runtime.runtime.runtimeEffect), [props.runtime]), + children: props.children, +}) +SyncProvider.displayName = "ReactManagedRuntimeSyncProvider" diff --git a/packages/effect-components/src/index.ts b/packages/effect-components/src/index.ts index 3db5a6c..8b8a4b1 100644 --- a/packages/effect-components/src/index.ts +++ b/packages/effect-components/src/index.ts @@ -1,2 +1,3 @@ export * as ReactComponent from "./ReactComponent.js" export * as ReactHook from "./ReactHook.js" +export * as ReactManagedRuntime from "./ReactManagedRuntime.js" diff --git a/packages/example/src/routes/effect-component-tests.tsx b/packages/example/src/routes/effect-component-tests.tsx index 8eb1c22..3dadb32 100644 --- a/packages/example/src/routes/effect-component-tests.tsx +++ b/packages/example/src/routes/effect-component-tests.tsx @@ -1,44 +1,56 @@ -import { Box, Text, TextField } from "@radix-ui/themes" +import { Box, TextField } from "@radix-ui/themes" import { createFileRoute } from "@tanstack/react-router" -import { Console, Effect, Layer, ManagedRuntime, SubscriptionRef } from "effect" -import { ReactComponent, ReactHook } from "effect-components" +import { Console, Effect, Layer, pipe, SubscriptionRef } from "effect" +import { ReactComponent, ReactHook, ReactManagedRuntime } from "effect-components" import * as React from "react" +class TestService extends Effect.Service()("TestService", { + effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")), +}) {} + +const runtime = ReactManagedRuntime.make(Layer.empty) + + export const Route = createFileRoute("/effect-component-tests")({ component: RouteComponent, }) function RouteComponent() { - const runtime = React.useMemo(() => ManagedRuntime.make(Layer.empty), []) - - return <> - {runtime.runSync(ReactComponent.use(MyTestComponent, Component => ( - - )))} - + return ( + + + + ) } +const MyRoute = pipe( + Effect.fn(function*() { + return yield* ReactComponent.use(MyTestComponent, C => ) + }), + ReactComponent.withDisplayName("MyRoute"), + ReactComponent.withRuntime(runtime.context), +) -class TestService extends Effect.Service()("TestService", { - effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")), -}) {} -const MyTestComponent = Effect.fn(function* MyTestComponent(props?: { readonly value?: string }) { - const [state, setState] = React.useState("value") +const MyTestComponent = pipe( + Effect.fn(function*() { + const [state, setState] = React.useState("value") - // yield* ReactHook.useEffect(() => Effect.andThen( - // Effect.addFinalizer(() => Console.log("MyTestComponent umounted")), - // Console.log("MyTestComponent mounted"), - // ), []) + yield* ReactHook.useEffect(() => Effect.andThen( + Effect.addFinalizer(() => Console.log("MyTestComponent umounted")), + Console.log("MyTestComponent mounted"), + ), []) - return <> - - setState(e.target.value)} - /> - - -}) -console.log(MyTestComponent) + return <> + + setState(e.target.value)} + /> + + + }), + + ReactComponent.withDisplayName("MyTestComponent"), +)