From 605c0f9d57487210979bd22eeede4885095f16f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Fri, 18 Jul 2025 16:26:47 +0200 Subject: [PATCH 01/28] Tanstack TMP fix --- packages/example/.gitignore | 2 ++ .../dfef8bd1830fa25b780317b9e88c0651 | 9 --------- 2 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 packages/example/.tanstack/tmp/router-generator-K3vMsf/dfef8bd1830fa25b780317b9e88c0651 diff --git a/packages/example/.gitignore b/packages/example/.gitignore index a547bf3..a014dba 100644 --- a/packages/example/.gitignore +++ b/packages/example/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.tanstack diff --git a/packages/example/.tanstack/tmp/router-generator-K3vMsf/dfef8bd1830fa25b780317b9e88c0651 b/packages/example/.tanstack/tmp/router-generator-K3vMsf/dfef8bd1830fa25b780317b9e88c0651 deleted file mode 100644 index 36ccbf1..0000000 --- a/packages/example/.tanstack/tmp/router-generator-K3vMsf/dfef8bd1830fa25b780317b9e88c0651 +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/dev/memo')({ - component: RouteComponent, -}) - -function RouteComponent() { - return
Hello "/dev/memo"!
-} -- 2.49.1 From c9df6e7a88236090674a20939105ec5fd7b7f14d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 21 Jul 2025 01:24:14 +0200 Subject: [PATCH 02/28] memoWithOptions --- packages/effect-fc/src/Component.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 85331ac..7e6d4a2 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -370,21 +370,21 @@ export const memo = >( Object.getPrototypeOf(self), ) -export const memoWithEquivalence: { +export const memoWithOptions: { >( - propsAreEqual: Equivalence.Equivalence> + memoOptions: Partial>> ): ( self: ExcludeKeys>> ) => T & Memoized> >( self: ExcludeKeys>>, - propsAreEqual: Equivalence.Equivalence>, + memoOptions: Partial>>, ): T & Memoized> } = Function.dual(2, >( self: ExcludeKeys>>, - propsAreEqual: Equivalence.Equivalence>, + memoOptions: Partial>>, ): T & Memoized> => Object.setPrototypeOf( - { ...self, memo: true, memoOptions: { propsAreEqual } }, + { ...self, memo: true, memoOptions }, Object.getPrototypeOf(self), )) -- 2.49.1 From 5f34ea8df5bdb52f2b7931c56aebc43bee173d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 21 Jul 2025 01:46:13 +0200 Subject: [PATCH 03/28] Work --- packages/effect-fc/src/Component.ts | 46 ++++++++++++++--------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 7e6d4a2..3b484f2 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -328,29 +328,6 @@ export const withOptions: { Object.getPrototypeOf(self), )) -export const withRuntime: { - , R>( - context: React.Context>, - ): (self: T) => React.FC & SuspenseProps - : Component.Props - > - ( - self: Component & Suspense, - context: React.Context>, - ): React.FC

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

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

=> function WithRuntime(props) { - const runtime = React.useContext(context) - return React.createElement(Runtime.runSync(runtime)(useFC(self)), props) -}) - export interface Memoized

{ readonly memo: true @@ -403,6 +380,29 @@ export const suspense = , P extends {}>( ) +export const withRuntime: { + , R>( + context: React.Context>, + ): (self: T) => React.FC & SuspenseProps + : Component.Props + > + ( + self: Component & Suspense, + context: React.Context>, + ): React.FC

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

+} = Function.dual(2, ( + self: Component, + 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: { ( self: Component & Suspense -- 2.49.1 From f7534d63f82faf8254c4fa72b269af28770c8992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 21 Jul 2025 01:50:00 +0200 Subject: [PATCH 04/28] Work --- packages/effect-fc/src/Component.ts | 46 ++++++++++++++--------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 3b484f2..c692bfa 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -380,29 +380,6 @@ export const suspense = , P extends {}>( ) -export const withRuntime: { - , R>( - context: React.Context>, - ): (self: T) => React.FC & SuspenseProps - : Component.Props - > - ( - self: Component & Suspense, - context: React.Context>, - ): React.FC

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

-} = Function.dual(2, ( - self: Component, - 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: { ( self: Component & Suspense @@ -469,3 +446,26 @@ export const use: { } = Effect.fn("use")(function*(self, fn) { return fn(yield* useFC(self)) }) + +export const withRuntime: { + , R>( + context: React.Context>, + ): (self: T) => React.FC & SuspenseProps + : Component.Props + > + ( + self: Component & Suspense, + context: React.Context>, + ): React.FC

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

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

=> function WithRuntime(props) { + const runtime = React.useContext(context) + return React.createElement(Runtime.runSync(runtime)(useFC(self)), props) +}) -- 2.49.1 From 30bd1a0180883faf3e58bf8fcdf1c11ba40d12d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 21 Jul 2025 11:19:55 +0200 Subject: [PATCH 05/28] Suspense refactoring --- packages/effect-fc/src/Component.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index c692bfa..5a7f6cb 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -368,14 +368,21 @@ export const memoWithOptions: { export interface Suspense { readonly suspense: true + readonly suspenseOptions: Suspense.Options } -export type SuspenseProps = Omit +export namespace Suspense { + export interface Options { + readonly defaultFallback?: React.ReactNode + } + + export type Props = Omit +} export const suspense = , P extends {}>( - self: ExcludeKeys & Component> + self: ExcludeKeys & Component> ): T & Suspense => Object.setPrototypeOf( - { ...self, suspense: true }, + { ...self, suspense: true, suspenseOptions: {} }, Object.getPrototypeOf(self), ) @@ -383,7 +390,7 @@ export const suspense = , P extends {}>( export const useFC: { ( self: Component & Suspense - ): Effect.Effect, never, Exclude> + ): Effect.Effect, never, Exclude> ( self: Component ): Effect.Effect, never, Exclude> @@ -408,14 +415,14 @@ export const useFC: { return React.use(props.promise) }, - SuspenseInner => ({ fallback, name, ...props }: P & SuspenseProps) => { + SuspenseInner => ({ fallback, name, ...props }: P & Suspense.Props) => { const promise = Runtime.runPromise(runtimeRef.current)( Effect.provideService(self.body(props as P), Scope.Scope, scope) ) return React.createElement( React.Suspense, - { fallback, name }, + { fallback: fallback ?? self.suspenseOptions.defaultFallback, name }, React.createElement(SuspenseInner, { promise }), ) }, @@ -437,7 +444,7 @@ export const useFC: { export const use: { ( self: Component & Suspense, - fn: (Component: React.FC

) => React.ReactNode, + fn: (Component: React.FC

) => React.ReactNode, ): Effect.Effect> ( self: Component, @@ -451,13 +458,13 @@ export const withRuntime: { , R>( context: React.Context>, ): (self: T) => React.FC & SuspenseProps + ? Component.Props & Suspense.Props : Component.Props > ( self: Component & Suspense, context: React.Context>, - ): React.FC

+ ): React.FC

( self: Component, context: React.Context>, -- 2.49.1 From b5f081044ec473556ea8e8ba25faf4a494392fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 21 Jul 2025 11:35:02 +0200 Subject: [PATCH 06/28] Add suspenseWithOptions --- packages/effect-fc/src/Component.ts | 18 ++++++++++++++++++ .../example/src/routes/dev/async-rendering.tsx | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 5a7f6cb..280cad0 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -386,6 +386,24 @@ export const suspense = , P extends {}>( Object.getPrototypeOf(self), ) +export const suspenseWithOptions: { + , P extends {}>( + suspenseOptions: Partial + ): ( + self: ExcludeKeys & Component> + ) => T & Suspense + , P extends {}>( + self: ExcludeKeys & Component>, + suspenseOptions: Partial, + ): T & Suspense +} = Function.dual(2, , P extends {}>( + self: ExcludeKeys & Component>, + suspenseOptions: Partial, +): T & Suspense => Object.setPrototypeOf( + { ...self, suspense: true, suspenseOptions }, + Object.getPrototypeOf(self), +)) + export const useFC: { ( diff --git a/packages/example/src/routes/dev/async-rendering.tsx b/packages/example/src/routes/dev/async-rendering.tsx index 7b123c9..4e6c957 100644 --- a/packages/example/src/routes/dev/async-rendering.tsx +++ b/packages/example/src/routes/dev/async-rendering.tsx @@ -20,7 +20,7 @@ const RouteComponent = Component.make(function* AsyncRendering() { onChange={e => setInput(e.target.value)} /> - + Loading memoized...

} /> ) @@ -61,7 +61,7 @@ const AsyncComponent = Component.make(function* AsyncComponent() { ) }).pipe( - Component.suspense + Component.suspenseWithOptions({ defaultFallback:

Loading...

}) ) const MemoizedAsyncComponent = Component.memo(AsyncComponent) -- 2.49.1 From 77abffc8ff8f5c32811a446192b87f1341ec178b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 21 Jul 2025 21:55:19 +0200 Subject: [PATCH 07/28] Memo refactoring --- packages/effect-fc/src/Component.ts | 35 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 280cad0..1052e49 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -340,28 +340,27 @@ export namespace Memoized { } } -export const memo = >( - self: ExcludeKeys>> -): T & Memoized> => Object.setPrototypeOf( - { ...self, memo: true, memoOptions: {} }, - Object.getPrototypeOf(self), -) +export const memo = | Component & Memoized>( + self: T +): T & Memoized> => Object.setPrototypeOf({ + ...self, + memo: true, + memoOptions: Predicate.hasProperty(self, "memo") ? { ...self.memoOptions } : {}, +}, Object.getPrototypeOf(self)) -export const memoWithOptions: { - >( +export const withMemoOptions: { + & Memoized>( memoOptions: Partial>> - ): ( - self: ExcludeKeys>> - ) => T & Memoized> - >( - self: ExcludeKeys>>, + ): (self: T) => T + & Memoized>( + self: T, memoOptions: Partial>>, - ): T & Memoized> -} = Function.dual(2, >( - self: ExcludeKeys>>, + ): T +} = Function.dual(2, & Memoized>( + self: T, memoOptions: Partial>>, -): T & Memoized> => Object.setPrototypeOf( - { ...self, memo: true, memoOptions }, +): T => Object.setPrototypeOf( + { ...self, memoOptions: { ...self.memoOptions, ...memoOptions } }, Object.getPrototypeOf(self), )) -- 2.49.1 From 3484664832e51d41acccf07fa19dbe13ab084801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 22 Jul 2025 00:54:59 +0200 Subject: [PATCH 08/28] Refactoring --- packages/effect-fc/src/Component.ts | 43 ++++++++++++++---------- packages/example/src/routes/dev/memo.tsx | 9 +++-- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 1052e49..5168910 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -21,6 +21,10 @@ export namespace Component { } } +export interface ComponentClass extends Component { + +} + const ComponentProto = Object.seal({ pipe() { return Pipeable.pipeArguments(this, arguments) } @@ -378,28 +382,31 @@ export namespace Suspense { export type Props = Omit } -export const suspense = , P extends {}>( - self: ExcludeKeys & Component> -): T & Suspense => Object.setPrototypeOf( - { ...self, suspense: true, suspenseOptions: {} }, - Object.getPrototypeOf(self), -) +export const suspense = | Component & Suspense, P extends {}>( + self: T & Component> +): ( + & T + & Component, Component.Context, P & Suspense.Props> + & Suspense +) => Object.setPrototypeOf({ + ...self, + suspense: true, + suspenseOptions: Predicate.hasProperty(self, "suspense") ? { ...self.suspenseOptions } : {}, +}, Object.getPrototypeOf(self)) -export const suspenseWithOptions: { - , P extends {}>( +export const withSuspenseOptions: { + & Suspense>( suspenseOptions: Partial - ): ( - self: ExcludeKeys & Component> - ) => T & Suspense - , P extends {}>( - self: ExcludeKeys & Component>, + ): (self: T) => T + & Suspense>( + self: T, suspenseOptions: Partial, - ): T & Suspense -} = Function.dual(2, , P extends {}>( - self: ExcludeKeys & Component>, + ): T +} = Function.dual(2, & Suspense>( + self: T, suspenseOptions: Partial, -): T & Suspense => Object.setPrototypeOf( - { ...self, suspense: true, suspenseOptions }, +): T => Object.setPrototypeOf( + { ...self, suspense: true, suspenseOptions: { ...self.suspenseOptions, ...suspenseOptions } }, Object.getPrototypeOf(self), )) diff --git a/packages/example/src/routes/dev/memo.tsx b/packages/example/src/routes/dev/memo.tsx index f58e6ad..b457d3d 100644 --- a/packages/example/src/routes/dev/memo.tsx +++ b/packages/example/src/routes/dev/memo.tsx @@ -28,12 +28,17 @@ const RouteComponent = Component.make(function* RouteComponent() { Component.withRuntime(runtime.context) ) -const SubComponent = Component.make(function* SubComponent() { +const SubComponent = Component.make(function* SubComponent(props: { readonly value?: string }) { const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom)) return {id} }) -const MemoizedSubComponent = Component.memo(SubComponent) +const MemoizedSubComponent = SubComponent.pipe( + Component.memo, + Component.suspense, + Component.memo, +) +type T = typeof MemoizedSubComponent extends Component.Memoized ? P : never export const Route = createFileRoute("/dev/memo")({ component: RouteComponent, -- 2.49.1 From e11bbff2348cb77c1210573781e5f8a6a3bb7d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 22 Jul 2025 01:57:41 +0200 Subject: [PATCH 09/28] Component as class --- packages/effect-fc/src/Component.ts | 6 ++---- packages/example/src/routes/dev/memo.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 5168910..0ae4a90 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -5,6 +5,8 @@ import type { ExcludeKeys } from "./utils.js" export interface Component extends Pipeable.Pipeable { + new(_: never): {} + readonly body: (props: P) => Effect.Effect readonly displayName?: string readonly options: Component.Options @@ -21,10 +23,6 @@ export namespace Component { } } -export interface ComponentClass extends Component { - -} - const ComponentProto = Object.seal({ pipe() { return Pipeable.pipeArguments(this, arguments) } diff --git a/packages/example/src/routes/dev/memo.tsx b/packages/example/src/routes/dev/memo.tsx index b457d3d..08208ca 100644 --- a/packages/example/src/routes/dev/memo.tsx +++ b/packages/example/src/routes/dev/memo.tsx @@ -28,16 +28,16 @@ const RouteComponent = Component.make(function* RouteComponent() { Component.withRuntime(runtime.context) ) -const SubComponent = Component.make(function* SubComponent(props: { readonly value?: string }) { +class SubComponent extends Component.make(function* SubComponent(props: { readonly value?: string }) { const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom)) return {id} -}) +}) {} -const MemoizedSubComponent = SubComponent.pipe( +class MemoizedSubComponent extends SubComponent.pipe( Component.memo, Component.suspense, Component.memo, -) +) {} type T = typeof MemoizedSubComponent extends Component.Memoized ? P : never export const Route = createFileRoute("/dev/memo")({ -- 2.49.1 From 44a8a96380ea67f5502d87d1ac1ed66adee19713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 22 Jul 2025 17:32:27 +0200 Subject: [PATCH 10/28] Component refactoring --- packages/effect-fc/src/Component.ts | 42 +++++++++++++++++------------ 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 0ae4a90..548182f 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -4,12 +4,15 @@ import * as Hook from "./Hook.js" import type { ExcludeKeys } from "./utils.js" -export interface Component extends Pipeable.Pipeable { +export const TypeId: unique symbol = Symbol.for("effect-fc/Component") +export type TypeId = typeof TypeId + +export interface Component extends Pipeable.Pipeable, Component.Options { new(_: never): {} + readonly [TypeId]: TypeId readonly body: (props: P) => Effect.Effect readonly displayName?: string - readonly options: Component.Options } export namespace Component { @@ -25,17 +28,22 @@ export namespace Component { const ComponentProto = Object.seal({ + [TypeId]: TypeId, pipe() { return Pipeable.pipeArguments(this, arguments) } } as const) -const defaultOptions: Component.Options = { - finalizerExecutionMode: "sync", - finalizerExecutionStrategy: ExecutionStrategy.sequential, -} +const makeWithDefaults = (): Component => Object.assign( + Object.setPrototypeOf(function() {}, ComponentProto), { + finalizerExecutionMode: "sync", + finalizerExecutionStrategy: ExecutionStrategy.sequential, + } +) const nonReactiveTags = [Tracer.ParentSpan] as const +export const isComponent = (u: unknown): u is Component => Predicate.hasProperty(u, TypeId) + export namespace make { export type Gen = { >, P extends {} = {}>( @@ -268,31 +276,31 @@ export const make: ( spanName: string, spanOptions?: Tracer.SpanOptions, ) => make.Gen & make.NonGen) -) = (spanNameOrBody: Function | string, ...pipeables: any[]) => { +) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => { if (typeof spanNameOrBody !== "string") { const displayName = displayNameFromBody(spanNameOrBody) - return Object.setPrototypeOf({ + return Object.assign(makeWithDefaults(), { body: displayName ? Effect.fn(displayName)(spanNameOrBody as any, ...pipeables as []) : Effect.fn(spanNameOrBody as any, ...pipeables), displayName, - options: { ...defaultOptions }, - }, ComponentProto) + }) } else { const spanOptions = pipeables[0] - return (body: any, ...pipeables: any[]) => Object.setPrototypeOf({ + return (body: any, ...pipeables: any[]) => Object.assign(makeWithDefaults(), { body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []), displayName: displayNameFromBody(body) ?? spanNameOrBody, - options: { ...defaultOptions }, - }, ComponentProto) + }) } } -export const makeUntraced: make.Gen & make.NonGen = (body: Function, ...pipeables: any[]) => Object.setPrototypeOf({ +export const makeUntraced: make.Gen & make.NonGen = ( + body: Function, + ...pipeables: any[] +) => Object.assign(makeWithDefaults(), { body: Effect.fnUntraced(body as any, ...pipeables as []), displayName: displayNameFromBody(body), - options: { ...defaultOptions }, }, ComponentProto) const displayNameFromBody = (body: Function) => !String.isEmpty(body.name) ? body.name : undefined @@ -326,7 +334,7 @@ export const withOptions: { self: T, options: Partial, ): T => Object.setPrototypeOf( - { ...self, options: { ...self.options, ...options } }, + { ...self, ...options }, Object.getPrototypeOf(self), )) @@ -427,7 +435,7 @@ export const useFC: { Array.from( Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values() ), - self.options, + self, )) const FC = React.useMemo(() => { -- 2.49.1 From aa8feb79fa7085c451ce33e504787a032fe6293a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 22 Jul 2025 19:26:53 +0200 Subject: [PATCH 11/28] Refactoring --- packages/effect-fc/src/Component.ts | 111 +++++----------------------- packages/effect-fc/src/Memoized.ts | 42 +++++++++++ packages/effect-fc/src/Suspense.ts | 49 ++++++++++++ packages/effect-fc/src/index.ts | 2 + 4 files changed, 110 insertions(+), 94 deletions(-) create mode 100644 packages/effect-fc/src/Memoized.ts create mode 100644 packages/effect-fc/src/Suspense.ts diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 548182f..1dbd9b7 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -1,7 +1,8 @@ -import { Context, Effect, type Equivalence, ExecutionStrategy, Function, pipe, Pipeable, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect" +import { Context, Effect, ExecutionStrategy, Function, pipe, Pipeable, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect" import * as React from "react" import * as Hook from "./Hook.js" -import type { ExcludeKeys } from "./utils.js" +import * as Memoized from "./Memoized.js" +import * as Suspense from "./Suspense.js" export const TypeId: unique symbol = Symbol.for("effect-fc/Component") @@ -339,93 +340,15 @@ export const withOptions: { )) -export interface Memoized

{ - readonly memo: true - readonly memoOptions: Memoized.Options

-} - -export namespace Memoized { - export interface Options

{ - readonly propsAreEqual?: Equivalence.Equivalence

- } -} - -export const memo = | Component & Memoized>( - self: T -): T & Memoized> => Object.setPrototypeOf({ - ...self, - memo: true, - memoOptions: Predicate.hasProperty(self, "memo") ? { ...self.memoOptions } : {}, -}, Object.getPrototypeOf(self)) - -export const withMemoOptions: { - & Memoized>( - memoOptions: Partial>> - ): (self: T) => T - & Memoized>( - self: T, - memoOptions: Partial>>, - ): T -} = Function.dual(2, & Memoized>( - self: T, - memoOptions: Partial>>, -): T => Object.setPrototypeOf( - { ...self, memoOptions: { ...self.memoOptions, ...memoOptions } }, - Object.getPrototypeOf(self), -)) - - -export interface Suspense { - readonly suspense: true - readonly suspenseOptions: Suspense.Options -} - -export namespace Suspense { - export interface Options { - readonly defaultFallback?: React.ReactNode - } - - export type Props = Omit -} - -export const suspense = | Component & Suspense, P extends {}>( - self: T & Component> -): ( - & T - & Component, Component.Context, P & Suspense.Props> - & Suspense -) => Object.setPrototypeOf({ - ...self, - suspense: true, - suspenseOptions: Predicate.hasProperty(self, "suspense") ? { ...self.suspenseOptions } : {}, -}, Object.getPrototypeOf(self)) - -export const withSuspenseOptions: { - & Suspense>( - suspenseOptions: Partial - ): (self: T) => T - & Suspense>( - self: T, - suspenseOptions: Partial, - ): T -} = Function.dual(2, & Suspense>( - self: T, - suspenseOptions: Partial, -): T => Object.setPrototypeOf( - { ...self, suspense: true, suspenseOptions: { ...self.suspenseOptions, ...suspenseOptions } }, - Object.getPrototypeOf(self), -)) - - export const useFC: { ( - self: Component & Suspense - ): Effect.Effect, never, Exclude> + self: Component & Suspense.Suspense + ): Effect.Effect, never, Exclude> ( self: Component ): Effect.Effect, never, Exclude> } = Effect.fn("useFC")(function* ( - self: Component & (Memoized

| Suspense | {}) + self: Component & (Memoized.Memoized

| Suspense.Suspense | {}) ) { const runtimeRef = React.useRef>>(null!) runtimeRef.current = yield* Effect.runtime>() @@ -439,20 +362,20 @@ export const useFC: { )) const FC = React.useMemo(() => { - const f: React.FC

= Predicate.hasProperty(self, "suspense") + const f: React.FC

= Suspense.isSuspense(self) ? pipe( function SuspenseInner(props: { readonly promise: Promise }) { return React.use(props.promise) }, - SuspenseInner => ({ fallback, name, ...props }: P & Suspense.Props) => { + SuspenseInner => ({ fallback, name, ...props }: P & Suspense.Suspense.Props) => { const promise = Runtime.runPromise(runtimeRef.current)( Effect.provideService(self.body(props as P), Scope.Scope, scope) ) return React.createElement( React.Suspense, - { fallback: fallback ?? self.suspenseOptions.defaultFallback, name }, + { fallback: fallback ?? self.defaultFallback, name }, React.createElement(SuspenseInner, { promise }), ) }, @@ -462,8 +385,8 @@ export const useFC: { ) f.displayName = self.displayName ?? "Anonymous" - return Predicate.hasProperty(self, "memo") - ? React.memo(f, self.memoOptions.propsAreEqual) + return Memoized.isMemoized(self) + ? React.memo(f, self.propsAreEqual) : f }, [scope]) @@ -473,8 +396,8 @@ export const useFC: { export const use: { ( - self: Component & Suspense, - fn: (Component: React.FC

) => React.ReactNode, + self: Component & Suspense.Suspense, + fn: (Component: React.FC

) => React.ReactNode, ): Effect.Effect> ( self: Component, @@ -487,14 +410,14 @@ export const use: { export const withRuntime: { , R>( context: React.Context>, - ): (self: T) => React.FC & Suspense.Props + ): (self: T) => React.FC & Suspense.Suspense.Props : Component.Props > ( - self: Component & Suspense, + self: Component & Suspense.Suspense, context: React.Context>, - ): React.FC

+ ): React.FC

( self: Component, context: React.Context>, diff --git a/packages/effect-fc/src/Memoized.ts b/packages/effect-fc/src/Memoized.ts new file mode 100644 index 0000000..4400c34 --- /dev/null +++ b/packages/effect-fc/src/Memoized.ts @@ -0,0 +1,42 @@ +import { type Equivalence, Function, Predicate } from "effect" +import type * as Component from "./Component.js" + + +export const TypeId: unique symbol = Symbol.for("effect-fc/Memoized") +export type TypeId = typeof TypeId + +export interface Memoized

extends Memoized.Options

{ + readonly [TypeId]: true +} + +export namespace Memoized { + export interface Options

{ + readonly propsAreEqual?: Equivalence.Equivalence

+ } +} + + +export const isMemoized = (u: unknown): u is Memoized => Predicate.hasProperty(u, TypeId) + +export const memo = >( + self: T +): T & Memoized> => Object.setPrototypeOf( + { ...self, [TypeId]: true }, + Object.getPrototypeOf(self), +) + +export const withOptions: { + & Memoized>( + options: Partial>> + ): (self: T) => T + & Memoized>( + self: T, + options: Partial>>, + ): T +} = Function.dual(2, & Memoized>( + self: T, + options: Partial>>, +): T => Object.setPrototypeOf( + { ...self, ...options }, + Object.getPrototypeOf(self), +)) diff --git a/packages/effect-fc/src/Suspense.ts b/packages/effect-fc/src/Suspense.ts new file mode 100644 index 0000000..b6aeba8 --- /dev/null +++ b/packages/effect-fc/src/Suspense.ts @@ -0,0 +1,49 @@ +import { Function, Predicate } from "effect" +import type * as Component from "./Component.js" +import type { ExcludeKeys } from "./utils.js" + + +export const TypeId: unique symbol = Symbol.for("effect-fc/Suspense") +export type TypeId = typeof TypeId + +export interface Suspense extends Suspense.Options { + readonly [TypeId]: true +} + +export namespace Suspense { + export interface Options { + readonly defaultFallback?: React.ReactNode + } + + export type Props = Omit +} + + +export const isSuspense = (u: unknown): u is Suspense => Predicate.hasProperty(u, TypeId) + +export const suspense = , P extends {}>( + self: T & Component.Component> +): ( + & T + & Component.Component, Component.Component.Context, P & Suspense.Props> + & Suspense +) => Object.setPrototypeOf( + { ...self, [TypeId]: true }, + Object.getPrototypeOf(self), +) + +export const withOptions: { + & Suspense>( + options: Partial + ): (self: T) => T + & Suspense>( + self: T, + options: Partial, + ): T +} = Function.dual(2, & Suspense>( + self: T, + options: Partial, +): T => Object.setPrototypeOf( + { ...self, ...options }, + Object.getPrototypeOf(self), +)) diff --git a/packages/effect-fc/src/index.ts b/packages/effect-fc/src/index.ts index 4318eff..fd1ade9 100644 --- a/packages/effect-fc/src/index.ts +++ b/packages/effect-fc/src/index.ts @@ -1,3 +1,5 @@ export * as Component from "./Component.js" export * as Hook from "./Hook.js" +export * as Memoized from "./Memoized.js" export * as ReactManagedRuntime from "./ReactManagedRuntime.js" +export * as Suspense from "./Suspense.js" -- 2.49.1 From 8737e8893b953d93cd233a332b720f1eb9fb8d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 22 Jul 2025 20:22:47 +0200 Subject: [PATCH 12/28] Cleanup --- packages/effect-fc/src/Component.ts | 18 ++---------------- packages/example/src/routes/dev/memo.tsx | 10 +++++----- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 1dbd9b7..84abe11 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -341,14 +341,11 @@ export const withOptions: { export const useFC: { - ( - self: Component & Suspense.Suspense - ): Effect.Effect, never, Exclude> ( self: Component ): Effect.Effect, never, Exclude> } = Effect.fn("useFC")(function* ( - self: Component & (Memoized.Memoized

| Suspense.Suspense | {}) + self: Component ) { const runtimeRef = React.useRef>>(null!) runtimeRef.current = yield* Effect.runtime>() @@ -395,10 +392,6 @@ export const useFC: { }) export const use: { - ( - self: Component & Suspense.Suspense, - fn: (Component: React.FC

) => React.ReactNode, - ): Effect.Effect> ( self: Component, fn: (Component: React.FC

) => React.ReactNode, @@ -408,16 +401,9 @@ export const use: { }) export const withRuntime: { - , R>( - context: React.Context>, - ): (self: T) => React.FC & Suspense.Suspense.Props - : Component.Props - > ( - self: Component & Suspense.Suspense, context: React.Context>, - ): React.FC

+ ): (self: Component) => React.FC

( self: Component, context: React.Context>, diff --git a/packages/example/src/routes/dev/memo.tsx b/packages/example/src/routes/dev/memo.tsx index 08208ca..3ae3fa3 100644 --- a/packages/example/src/routes/dev/memo.tsx +++ b/packages/example/src/routes/dev/memo.tsx @@ -3,7 +3,7 @@ import { Flex, Text, TextField } from "@radix-ui/themes" import { createFileRoute } from "@tanstack/react-router" import { GetRandomValues, makeUuid4 } from "@typed/id" import { Effect } from "effect" -import { Component } from "effect-fc" +import { Component, Memoized, Suspense } from "effect-fc" import * as React from "react" @@ -34,11 +34,11 @@ class SubComponent extends Component.make(function* SubComponent(props: { readon }) {} class MemoizedSubComponent extends SubComponent.pipe( - Component.memo, - Component.suspense, - Component.memo, + Memoized.memo, + Suspense.suspense, + Memoized.memo, ) {} -type T = typeof MemoizedSubComponent extends Component.Memoized ? P : never +type T = typeof MemoizedSubComponent extends Memoized.Memoized ? P : never export const Route = createFileRoute("/dev/memo")({ component: RouteComponent, -- 2.49.1 From 4a20d5879610da6b6cd28d51dbfac41c0b5970c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 22 Jul 2025 20:43:21 +0200 Subject: [PATCH 13/28] Cleanup --- packages/effect-fc/src/Component.ts | 20 +------------------- packages/example/src/routes/dev/memo.tsx | 11 +++-------- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 84abe11..4ff4b88 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -10,10 +10,8 @@ export type TypeId = typeof TypeId export interface Component extends Pipeable.Pipeable, Component.Options { new(_: never): {} - readonly [TypeId]: TypeId readonly body: (props: P) => Effect.Effect - readonly displayName?: string } export namespace Component { @@ -22,6 +20,7 @@ export namespace Component { export type Props = T extends Component ? P : never export interface Options { + readonly displayName?: string readonly finalizerExecutionMode: "sync" | "fork" readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy } @@ -306,23 +305,6 @@ export const makeUntraced: make.Gen & make.NonGen = ( const displayNameFromBody = (body: Function) => !String.isEmpty(body.name) ? body.name : undefined - -export const withDisplayName: { - >( - displayName: string - ): (self: T) => T - >( - self: T, - displayName: string, - ): T -} = Function.dual(2, >( - self: T, - displayName: string, -): T => Object.setPrototypeOf( - { ...self, displayName }, - Object.getPrototypeOf(self), -)) - export const withOptions: { >( options: Partial diff --git a/packages/example/src/routes/dev/memo.tsx b/packages/example/src/routes/dev/memo.tsx index 3ae3fa3..c325bc8 100644 --- a/packages/example/src/routes/dev/memo.tsx +++ b/packages/example/src/routes/dev/memo.tsx @@ -3,7 +3,7 @@ import { Flex, Text, TextField } from "@radix-ui/themes" import { createFileRoute } from "@tanstack/react-router" import { GetRandomValues, makeUuid4 } from "@typed/id" import { Effect } from "effect" -import { Component, Memoized, Suspense } from "effect-fc" +import { Component, Memoized } from "effect-fc" import * as React from "react" @@ -28,17 +28,12 @@ const RouteComponent = Component.make(function* RouteComponent() { Component.withRuntime(runtime.context) ) -class SubComponent extends Component.make(function* SubComponent(props: { readonly value?: string }) { +class SubComponent extends Component.make(function* SubComponent() { const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom)) return {id} }) {} -class MemoizedSubComponent extends SubComponent.pipe( - Memoized.memo, - Suspense.suspense, - Memoized.memo, -) {} -type T = typeof MemoizedSubComponent extends Memoized.Memoized ? P : never +class MemoizedSubComponent extends Memoized.memo(SubComponent) {} export const Route = createFileRoute("/dev/memo")({ component: RouteComponent, -- 2.49.1 From 6e517f36ea9bd10c235ad16f407e4be856effc84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 22 Jul 2025 22:36:08 +0200 Subject: [PATCH 14/28] Refactoring --- packages/effect-fc/src/Component.ts | 41 +++++++++++------------------ packages/effect-fc/src/Memoized.ts | 9 +++++-- packages/effect-fc/src/Suspense.ts | 31 +++++++++++++++++++--- 3 files changed, 50 insertions(+), 31 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 4ff4b88..f733015 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -1,8 +1,7 @@ -import { Context, Effect, ExecutionStrategy, Function, pipe, Pipeable, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect" +import { Context, Effect, ExecutionStrategy, Function, Pipeable, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect" import * as React from "react" import * as Hook from "./Hook.js" import * as Memoized from "./Memoized.js" -import * as Suspense from "./Suspense.js" export const TypeId: unique symbol = Symbol.for("effect-fc/Component") @@ -11,6 +10,7 @@ export type TypeId = typeof TypeId export interface Component extends Pipeable.Pipeable, Component.Options { new(_: never): {} readonly [TypeId]: TypeId + makeFunctionComponent(runtimeRef: React.Ref>>, scope: Scope.Scope): React.FC

readonly body: (props: P) => Effect.Effect } @@ -27,9 +27,19 @@ export namespace Component { } -const ComponentProto = Object.seal({ +const ComponentProto = Object.freeze({ [TypeId]: TypeId, - pipe() { return Pipeable.pipeArguments(this, arguments) } + pipe() { return Pipeable.pipeArguments(this, arguments) }, + + makeFunctionComponent( + this: Component, + runtimeRef: React.RefObject>, + scope: Scope.Scope, + ): React.FC { + return (props: any) => Runtime.runSync(runtimeRef.current)( + Effect.provideService(this.body(props), Scope.Scope, scope) + ) + }, } as const) const makeWithDefaults = (): Component => Object.assign( @@ -341,28 +351,7 @@ export const useFC: { )) const FC = React.useMemo(() => { - const f: React.FC

= Suspense.isSuspense(self) - ? pipe( - function SuspenseInner(props: { readonly promise: Promise }) { - return React.use(props.promise) - }, - - SuspenseInner => ({ fallback, name, ...props }: P & Suspense.Suspense.Props) => { - const promise = Runtime.runPromise(runtimeRef.current)( - Effect.provideService(self.body(props as P), Scope.Scope, scope) - ) - - return React.createElement( - React.Suspense, - { fallback: fallback ?? self.defaultFallback, name }, - React.createElement(SuspenseInner, { promise }), - ) - }, - ) - : (props: P) => Runtime.runSync(runtimeRef.current)( - Effect.provideService(self.body(props), Scope.Scope, scope) - ) - + const f = self.makeFunctionComponent(runtimeRef, scope) f.displayName = self.displayName ?? "Anonymous" return Memoized.isMemoized(self) ? React.memo(f, self.propsAreEqual) diff --git a/packages/effect-fc/src/Memoized.ts b/packages/effect-fc/src/Memoized.ts index 4400c34..0fadba3 100644 --- a/packages/effect-fc/src/Memoized.ts +++ b/packages/effect-fc/src/Memoized.ts @@ -6,7 +6,7 @@ export const TypeId: unique symbol = Symbol.for("effect-fc/Memoized") export type TypeId = typeof TypeId export interface Memoized

extends Memoized.Options

{ - readonly [TypeId]: true + readonly [TypeId]: TypeId } export namespace Memoized { @@ -16,12 +16,17 @@ export namespace Memoized { } +const MemoizedProto = Object.freeze({ + [TypeId]: TypeId +} as const) + + export const isMemoized = (u: unknown): u is Memoized => Predicate.hasProperty(u, TypeId) export const memo = >( self: T ): T & Memoized> => Object.setPrototypeOf( - { ...self, [TypeId]: true }, + { ...self, ...MemoizedProto }, Object.getPrototypeOf(self), ) diff --git a/packages/effect-fc/src/Suspense.ts b/packages/effect-fc/src/Suspense.ts index b6aeba8..93a7585 100644 --- a/packages/effect-fc/src/Suspense.ts +++ b/packages/effect-fc/src/Suspense.ts @@ -1,4 +1,5 @@ -import { Function, Predicate } from "effect" +import { Effect, Function, Predicate, Runtime, Scope } from "effect" +import * as React from "react" import type * as Component from "./Component.js" import type { ExcludeKeys } from "./utils.js" @@ -7,7 +8,7 @@ export const TypeId: unique symbol = Symbol.for("effect-fc/Suspense") export type TypeId = typeof TypeId export interface Suspense extends Suspense.Options { - readonly [TypeId]: true + readonly [TypeId]: TypeId } export namespace Suspense { @@ -19,6 +20,30 @@ export namespace Suspense { } +const SuspenseProto = Object.freeze({ + [TypeId]: TypeId, + makeFunctionComponent( + this: Component.Component & Suspense, + runtimeRef: React.RefObject>, + scope: Scope.Scope, + ): React.FC { + const SuspenseInner = (props: { readonly promise: Promise }) => React.use(props.promise) + + return ({ fallback, name, ...props }: Suspense.Props) => { + const promise = Runtime.runPromise(runtimeRef.current)( + Effect.provideService(this.body(props), Scope.Scope, scope) + ) + + return React.createElement( + React.Suspense, + { fallback: fallback ?? this.defaultFallback, name }, + React.createElement(SuspenseInner, { promise }), + ) + } + }, +} as const) + + export const isSuspense = (u: unknown): u is Suspense => Predicate.hasProperty(u, TypeId) export const suspense = , P extends {}>( @@ -28,7 +53,7 @@ export const suspense = , P extends { & Component.Component, Component.Component.Context, P & Suspense.Props> & Suspense ) => Object.setPrototypeOf( - { ...self, [TypeId]: true }, + { ...self, ...SuspenseProto }, Object.getPrototypeOf(self), ) -- 2.49.1 From 8ed88d54c3a4f6fa8dc6f5c5181f6aee39a46a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 22 Jul 2025 22:50:41 +0200 Subject: [PATCH 15/28] Example refactoring --- .../example/src/routes/dev/async-rendering.tsx | 15 ++++++++------- packages/example/src/todo/Todo.tsx | 8 ++++---- packages/example/src/todo/Todos.tsx | 4 ++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/example/src/routes/dev/async-rendering.tsx b/packages/example/src/routes/dev/async-rendering.tsx index 4e6c957..be03b9f 100644 --- a/packages/example/src/routes/dev/async-rendering.tsx +++ b/packages/example/src/routes/dev/async-rendering.tsx @@ -3,7 +3,7 @@ import { Flex, Text, TextField } from "@radix-ui/themes" import { createFileRoute } from "@tanstack/react-router" import { GetRandomValues, makeUuid4 } from "@typed/id" import { Effect } from "effect" -import { Component, Hook } from "effect-fc" +import { Component, Hook, Memoized, Suspense } from "effect-fc" import * as React from "react" @@ -50,7 +50,7 @@ const RouteComponent = Component.make(function* AsyncRendering() { // ) -const AsyncComponent = Component.make(function* AsyncComponent() { +class AsyncComponent extends Component.make(function* AsyncComponent() { const VSubComponent = yield* Component.useFC(SubComponent) yield* Effect.sleep("500 millis") @@ -61,14 +61,15 @@ const AsyncComponent = Component.make(function* AsyncComponent() { ) }).pipe( - Component.suspenseWithOptions({ defaultFallback:

Loading...

}) -) -const MemoizedAsyncComponent = Component.memo(AsyncComponent) + Suspense.suspense, + Suspense.withOptions({ defaultFallback:

Loading...

}), +) {} +class MemoizedAsyncComponent extends Memoized.memo(AsyncComponent) {} -const SubComponent = Component.make(function* SubComponent() { +class SubComponent extends Component.make(function* SubComponent() { const [state] = React.useState(yield* Hook.useOnce(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom))) return {state} -}) +}) {} export const Route = createFileRoute("/dev/async-rendering")({ component: RouteComponent diff --git a/packages/example/src/todo/Todo.tsx b/packages/example/src/todo/Todo.tsx index 3c8365a..69b82c8 100644 --- a/packages/example/src/todo/Todo.tsx +++ b/packages/example/src/todo/Todo.tsx @@ -2,7 +2,7 @@ import * as Domain from "@/domain" import { Box, Button, Flex, IconButton, TextArea } from "@radix-ui/themes" import { GetRandomValues, makeUuid4 } from "@typed/id" import { Chunk, Effect, Match, Option, Ref, Runtime, SubscriptionRef } from "effect" -import { Component, Hook } from "effect-fc" +import { Component, Hook, Memoized } from "effect-fc" import { SubscriptionSubRef } from "effect-fc/types" import { FaArrowDown, FaArrowUp } from "react-icons/fa" import { FaDeleteLeft } from "react-icons/fa6" @@ -24,7 +24,7 @@ export type TodoProps = ( | { readonly _tag: "edit", readonly index: number } ) -export const Todo = Component.make(function* Todo(props: TodoProps) { +export class Todo extends Component.make(function* Todo(props: TodoProps) { const runtime = yield* Effect.runtime() const state = yield* TodosState @@ -111,5 +111,5 @@ export const Todo = Component.make(function* Todo(props: TodoProps) { ) }).pipe( - Component.memo -) + Memoized.memo +) {} diff --git a/packages/example/src/todo/Todos.tsx b/packages/example/src/todo/Todos.tsx index aeec4e4..7529127 100644 --- a/packages/example/src/todo/Todos.tsx +++ b/packages/example/src/todo/Todos.tsx @@ -5,7 +5,7 @@ import { Todo } from "./Todo" import { TodosState } from "./TodosState.service" -export const Todos = Component.make(function* Todos() { +export class Todos extends Component.make(function* Todos() { const state = yield* TodosState const [todos] = yield* Hook.useSubscribeRefs(state.ref) @@ -29,4 +29,4 @@ export const Todos = Component.make(function* Todos() { ) -}) +}) {} -- 2.49.1 From 76b5ccd0e189b9125f869660cc8bbec0a0830842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 22 Jul 2025 23:14:12 +0200 Subject: [PATCH 16/28] Fix --- packages/effect-fc/src/Component.ts | 3 ++- packages/effect-fc/src/Memoized.ts | 4 ++-- packages/effect-fc/src/Suspense.ts | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index f733015..e860189 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -10,6 +10,7 @@ export type TypeId = typeof TypeId export interface Component extends Pipeable.Pipeable, Component.Options { new(_: never): {} readonly [TypeId]: TypeId + /** @internal */ makeFunctionComponent(runtimeRef: React.Ref>>, scope: Scope.Scope): React.FC

readonly body: (props: P) => Effect.Effect } @@ -327,7 +328,7 @@ export const withOptions: { self: T, options: Partial, ): T => Object.setPrototypeOf( - { ...self, ...options }, + Object.assign(function() {}, self, options), Object.getPrototypeOf(self), )) diff --git a/packages/effect-fc/src/Memoized.ts b/packages/effect-fc/src/Memoized.ts index 0fadba3..09b476e 100644 --- a/packages/effect-fc/src/Memoized.ts +++ b/packages/effect-fc/src/Memoized.ts @@ -26,7 +26,7 @@ export const isMemoized = (u: unknown): u is Memoized => Predicate.hasPrope export const memo = >( self: T ): T & Memoized> => Object.setPrototypeOf( - { ...self, ...MemoizedProto }, + Object.assign(function() {}, self, MemoizedProto), Object.getPrototypeOf(self), ) @@ -42,6 +42,6 @@ export const withOptions: { self: T, options: Partial>>, ): T => Object.setPrototypeOf( - { ...self, ...options }, + Object.assign(function() {}, self, options), Object.getPrototypeOf(self), )) diff --git a/packages/effect-fc/src/Suspense.ts b/packages/effect-fc/src/Suspense.ts index 93a7585..3b20fac 100644 --- a/packages/effect-fc/src/Suspense.ts +++ b/packages/effect-fc/src/Suspense.ts @@ -53,7 +53,7 @@ export const suspense = , P extends { & Component.Component, Component.Component.Context, P & Suspense.Props> & Suspense ) => Object.setPrototypeOf( - { ...self, ...SuspenseProto }, + Object.assign(function() {}, self, SuspenseProto), Object.getPrototypeOf(self), ) @@ -69,6 +69,6 @@ export const withOptions: { self: T, options: Partial, ): T => Object.setPrototypeOf( - { ...self, ...options }, + Object.assign(function() {}, self, options), Object.getPrototypeOf(self), )) -- 2.49.1 From 95674a8465452a82ab5d7304b4de6a85e00b8b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 22 Jul 2025 23:32:20 +0200 Subject: [PATCH 17/28] Fix --- packages/effect-fc/src/Component.ts | 42 +++++++++++++++++++---------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index e860189..2827d84 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -43,6 +43,11 @@ const ComponentProto = Object.freeze({ }, } as const) +const defaultOptions = { + finalizerExecutionMode: "sync", + finalizerExecutionStrategy: ExecutionStrategy.sequential, +} as const + const makeWithDefaults = (): Component => Object.assign( Object.setPrototypeOf(function() {}, ComponentProto), { finalizerExecutionMode: "sync", @@ -290,29 +295,38 @@ export const make: ( ) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => { if (typeof spanNameOrBody !== "string") { const displayName = displayNameFromBody(spanNameOrBody) - return Object.assign(makeWithDefaults(), { - body: displayName - ? Effect.fn(displayName)(spanNameOrBody as any, ...pipeables as []) - : Effect.fn(spanNameOrBody as any, ...pipeables), - displayName, - }) + return Object.setPrototypeOf( + Object.assign(function() {}, { + body: displayName + ? Effect.fn(displayName)(spanNameOrBody as any, ...pipeables as []) + : Effect.fn(spanNameOrBody as any, ...pipeables), + displayName, + }), + ComponentProto, + ) } else { const spanOptions = pipeables[0] - return (body: any, ...pipeables: any[]) => Object.assign(makeWithDefaults(), { - body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []), - displayName: displayNameFromBody(body) ?? spanNameOrBody, - }) + return (body: any, ...pipeables: any[]) => Object.setPrototypeOf( + Object.assign(function() {}, { + body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []), + displayName: displayNameFromBody(body) ?? spanNameOrBody, + }), + ComponentProto, + ) } } export const makeUntraced: make.Gen & make.NonGen = ( body: Function, ...pipeables: any[] -) => Object.assign(makeWithDefaults(), { - body: Effect.fnUntraced(body as any, ...pipeables as []), - displayName: displayNameFromBody(body), -}, ComponentProto) +) => Object.setPrototypeOf( + Object.assign(function() {}, { + body: Effect.fnUntraced(body as any, ...pipeables as []), + displayName: displayNameFromBody(body), + }), + ComponentProto, +) const displayNameFromBody = (body: Function) => !String.isEmpty(body.name) ? body.name : undefined -- 2.49.1 From 6b3df73ca39213a211eb1f3075b0acf89d917293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 23 Jul 2025 00:57:32 +0200 Subject: [PATCH 18/28] Fix --- packages/effect-fc/src/Component.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 2827d84..1fedbb9 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -48,13 +48,6 @@ const defaultOptions = { finalizerExecutionStrategy: ExecutionStrategy.sequential, } as const -const makeWithDefaults = (): Component => Object.assign( - Object.setPrototypeOf(function() {}, ComponentProto), { - finalizerExecutionMode: "sync", - finalizerExecutionStrategy: ExecutionStrategy.sequential, - } -) - const nonReactiveTags = [Tracer.ParentSpan] as const @@ -296,7 +289,7 @@ export const make: ( if (typeof spanNameOrBody !== "string") { const displayName = displayNameFromBody(spanNameOrBody) return Object.setPrototypeOf( - Object.assign(function() {}, { + Object.assign(function() {}, defaultOptions, { body: displayName ? Effect.fn(displayName)(spanNameOrBody as any, ...pipeables as []) : Effect.fn(spanNameOrBody as any, ...pipeables), @@ -308,7 +301,7 @@ export const make: ( else { const spanOptions = pipeables[0] return (body: any, ...pipeables: any[]) => Object.setPrototypeOf( - Object.assign(function() {}, { + Object.assign(function() {}, defaultOptions, { body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []), displayName: displayNameFromBody(body) ?? spanNameOrBody, }), @@ -321,7 +314,7 @@ export const makeUntraced: make.Gen & make.NonGen = ( body: Function, ...pipeables: any[] ) => Object.setPrototypeOf( - Object.assign(function() {}, { + Object.assign(function() {}, defaultOptions, { body: Effect.fnUntraced(body as any, ...pipeables as []), displayName: displayNameFromBody(body), }), -- 2.49.1 From 43d5a793dd0fa90ca74033a06f17db52e9239480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 23 Jul 2025 01:52:11 +0200 Subject: [PATCH 19/28] Fix --- packages/effect-fc/README.md | 28 ++++++++++++------- .../src/routes/dev/async-rendering.tsx | 2 +- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/effect-fc/README.md b/packages/effect-fc/README.md index 09f553e..260f32a 100644 --- a/packages/effect-fc/README.md +++ b/packages/effect-fc/README.md @@ -15,15 +15,12 @@ Documentation is currently being written. In the meantime, you can take a look a ## What writing components looks like ```typescript -import { Container, Flex, Heading } from "@radix-ui/themes" -import { Chunk, Console, Effect } from "effect" -import { Component, Hook } from "effect-fc" +import { Component, Hook, Memoized } from "effect-fc" import { Todo } from "./Todo" import { TodosState } from "./TodosState.service" +import { runtime } from "@/runtime" -// Component.Component -// VVV -export const Todos = Component.make(function* Todos() { +class Todos extends Component.make(function* Todos() { const state = yield* TodosState const [todos] = yield* Hook.useSubscribeRefs(state.ref) @@ -32,20 +29,31 @@ export const Todos = Component.make(function* Todos() { Effect.addFinalizer(() => Console.log("Todos unmounted")), )) - const VTodo = yield* Component.useFC(Todo) + const TodoFC = yield* Component.useFC(Todo) return ( Todos - + {Chunk.map(todos, (v, k) => - + )} ) -}) +}).pipe( + Memoized.memo +) {} + +const TodosEntrypoint = Component.make(function* TodosEntrypoint() { + const context = yield* Hook.useContext(TodosState.Default, { finalizerExecutionMode: "fork" }) + const TodosFC = yield* Effect.provide(Component.useFC(Todos), context) + + return +}).pipe( + Component.withRuntime(runtime.context) +) ``` diff --git a/packages/example/src/routes/dev/async-rendering.tsx b/packages/example/src/routes/dev/async-rendering.tsx index be03b9f..ecc52e6 100644 --- a/packages/example/src/routes/dev/async-rendering.tsx +++ b/packages/example/src/routes/dev/async-rendering.tsx @@ -20,7 +20,7 @@ const RouteComponent = Component.make(function* AsyncRendering() { onChange={e => setInput(e.target.value)} /> - Loading memoized...

} /> +

Loading memoized...

, [])} /> ) -- 2.49.1 From 5d045ecd1df25ce1993cb4a5fb41b4327f458eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 23 Jul 2025 02:00:02 +0200 Subject: [PATCH 20/28] Fix --- packages/effect-fc/src/Component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 1fedbb9..fbb6833 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -12,6 +12,7 @@ export interface Component extends Pipeable.Pipeable, Compon readonly [TypeId]: TypeId /** @internal */ makeFunctionComponent(runtimeRef: React.Ref>>, scope: Scope.Scope): React.FC

+ /** @internal */ readonly body: (props: P) => Effect.Effect } -- 2.49.1 From 7fdb2a799cdc833d47f4373000c3deca1a8fdfdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 23 Jul 2025 03:29:04 +0200 Subject: [PATCH 21/28] Refactoring --- packages/effect-fc/src/Component.ts | 34 ++++++++++++++++--- packages/effect-fc/src/Memoized.ts | 2 +- .../src/routes/dev/async-rendering.tsx | 4 +-- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index fbb6833..1ebd387 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -1,4 +1,4 @@ -import { Context, Effect, ExecutionStrategy, Function, Pipeable, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect" +import { Context, Effect, Effectable, ExecutionStrategy, Function, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect" import * as React from "react" import * as Hook from "./Hook.js" import * as Memoized from "./Memoized.js" @@ -7,7 +7,7 @@ import * as Memoized from "./Memoized.js" export const TypeId: unique symbol = Symbol.for("effect-fc/Component") export type TypeId = typeof TypeId -export interface Component extends Pipeable.Pipeable, Component.Options { +export interface Component extends Effect.Effect, never, R>, Component.Options { new(_: never): {} readonly [TypeId]: TypeId /** @internal */ @@ -30,8 +30,34 @@ export namespace Component { const ComponentProto = Object.freeze({ + ...Effectable.CommitPrototype, [TypeId]: TypeId, - pipe() { return Pipeable.pipeArguments(this, arguments) }, + + commit: Effect.fnUntraced(function* (this: Component) { + const self = this + + const runtimeRef = React.useRef>>(null!) + runtimeRef.current = yield* Effect.runtime>() + + return React.useCallback(function ScopeProvider(props: P) { + const scope = Runtime.runSync(runtimeRef.current)(Hook.useScope( + Array.from( + Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values() + ), + self, + )) + + const FC = React.useMemo(() => { + const f = self.makeFunctionComponent(runtimeRef, scope) + f.displayName = self.displayName ?? "Anonymous" + return Memoized.isMemoized(self) + ? React.memo(f, self.propsAreEqual) + : f + }, [scope]) + + return React.createElement(FC, props) + }, []) + }), makeFunctionComponent( this: Component, @@ -52,7 +78,7 @@ const defaultOptions = { const nonReactiveTags = [Tracer.ParentSpan] as const -export const isComponent = (u: unknown): u is Component => Predicate.hasProperty(u, TypeId) +export const isComponent = (u: unknown): u is Component => Predicate.hasProperty(u, TypeId) export namespace make { export type Gen = { diff --git a/packages/effect-fc/src/Memoized.ts b/packages/effect-fc/src/Memoized.ts index 09b476e..6358351 100644 --- a/packages/effect-fc/src/Memoized.ts +++ b/packages/effect-fc/src/Memoized.ts @@ -21,7 +21,7 @@ const MemoizedProto = Object.freeze({ } as const) -export const isMemoized = (u: unknown): u is Memoized => Predicate.hasProperty(u, TypeId) +export const isMemoized = (u: unknown): u is Memoized => Predicate.hasProperty(u, TypeId) export const memo = >( self: T diff --git a/packages/example/src/routes/dev/async-rendering.tsx b/packages/example/src/routes/dev/async-rendering.tsx index ecc52e6..aff109e 100644 --- a/packages/example/src/routes/dev/async-rendering.tsx +++ b/packages/example/src/routes/dev/async-rendering.tsx @@ -9,8 +9,8 @@ import * as React from "react" // Generator version const RouteComponent = Component.make(function* AsyncRendering() { - const VMemoizedAsyncComponent = yield* Component.useFC(MemoizedAsyncComponent) - const VAsyncComponent = yield* Component.useFC(AsyncComponent) + const VMemoizedAsyncComponent = yield* MemoizedAsyncComponent + const VAsyncComponent = yield* AsyncComponent const [input, setInput] = React.useState("") return ( -- 2.49.1 From 935a6f9e87e341d0694f1070ae05d3438e49e586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 23 Jul 2025 03:34:20 +0200 Subject: [PATCH 22/28] Fix --- packages/effect-fc/src/Component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 1ebd387..2bd8c6c 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -35,7 +35,6 @@ const ComponentProto = Object.freeze({ commit: Effect.fnUntraced(function* (this: Component) { const self = this - const runtimeRef = React.useRef>>(null!) runtimeRef.current = yield* Effect.runtime>() -- 2.49.1 From 3ce2a52d2b19750aa713d6af87c9702998206a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 23 Jul 2025 13:14:37 +0200 Subject: [PATCH 23/28] Cleanup --- packages/effect-fc/src/Component.ts | 45 ++----------------- .../src/routes/dev/async-rendering.tsx | 16 ++++--- packages/example/src/routes/dev/memo.tsx | 7 +-- packages/example/src/routes/index.tsx | 13 ++---- packages/example/src/todo/Todos.tsx | 6 +-- 5 files changed, 21 insertions(+), 66 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 2bd8c6c..236e895 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -7,7 +7,8 @@ import * as Memoized from "./Memoized.js" export const TypeId: unique symbol = Symbol.for("effect-fc/Component") export type TypeId = typeof TypeId -export interface Component extends Effect.Effect, never, R>, Component.Options { +export interface Component +extends Effect.Effect, never, Exclude>, Component.Options { new(_: never): {} readonly [TypeId]: TypeId /** @internal */ @@ -365,46 +366,6 @@ export const withOptions: { Object.getPrototypeOf(self), )) - -export const useFC: { - ( - self: Component - ): Effect.Effect, never, Exclude> -} = Effect.fn("useFC")(function* ( - self: Component -) { - const runtimeRef = React.useRef>>(null!) - runtimeRef.current = yield* Effect.runtime>() - - return React.useCallback(function ScopeProvider(props: P) { - const scope = Runtime.runSync(runtimeRef.current)(Hook.useScope( - Array.from( - Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values() - ), - self, - )) - - const FC = React.useMemo(() => { - const f = self.makeFunctionComponent(runtimeRef, scope) - f.displayName = self.displayName ?? "Anonymous" - return Memoized.isMemoized(self) - ? React.memo(f, self.propsAreEqual) - : f - }, [scope]) - - return React.createElement(FC, props) - }, []) -}) - -export const use: { - ( - self: Component, - fn: (Component: React.FC

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

=> function WithRuntime(props) { const runtime = React.useContext(context) - return React.createElement(Runtime.runSync(runtime)(useFC(self)), props) + return React.createElement(Runtime.runSync(runtime)(self), props) }) diff --git a/packages/example/src/routes/dev/async-rendering.tsx b/packages/example/src/routes/dev/async-rendering.tsx index aff109e..52b9fb1 100644 --- a/packages/example/src/routes/dev/async-rendering.tsx +++ b/packages/example/src/routes/dev/async-rendering.tsx @@ -9,8 +9,8 @@ import * as React from "react" // Generator version const RouteComponent = Component.make(function* AsyncRendering() { - const VMemoizedAsyncComponent = yield* MemoizedAsyncComponent - const VAsyncComponent = yield* AsyncComponent + const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent + const AsyncComponentFC = yield* AsyncComponent const [input, setInput] = React.useState("") return ( @@ -20,8 +20,8 @@ const RouteComponent = Component.make(function* AsyncRendering() { onChange={e => setInput(e.target.value)} /> -

Loading memoized...

, [])} /> - +

Loading memoized...

, [])} /> + ) }).pipe( @@ -51,13 +51,15 @@ const RouteComponent = Component.make(function* AsyncRendering() { class AsyncComponent extends Component.make(function* AsyncComponent() { - const VSubComponent = yield* Component.useFC(SubComponent) - yield* Effect.sleep("500 millis") + const SubComponentFC = yield* SubComponent + + yield* Effect.sleep("500 millis") // Async operation + // Cannot use React hooks after the async operation return ( Rendered! - + ) }).pipe( diff --git a/packages/example/src/routes/dev/memo.tsx b/packages/example/src/routes/dev/memo.tsx index c325bc8..42f2f41 100644 --- a/packages/example/src/routes/dev/memo.tsx +++ b/packages/example/src/routes/dev/memo.tsx @@ -8,9 +8,6 @@ import * as React from "react" const RouteComponent = Component.make(function* RouteComponent() { - const VSubComponent = yield* Component.useFC(SubComponent) - const VMemoizedSubComponent = yield* Component.useFC(MemoizedSubComponent) - const [value, setValue] = React.useState("") return ( @@ -20,8 +17,8 @@ const RouteComponent = Component.make(function* RouteComponent() { onChange={e => setValue(e.target.value)} /> - - + {yield* Effect.map(SubComponent, FC => )} + {yield* Effect.map(MemoizedSubComponent, FC => )} ) }).pipe( diff --git a/packages/example/src/routes/index.tsx b/packages/example/src/routes/index.tsx index 93e02d1..8c82d1b 100644 --- a/packages/example/src/routes/index.tsx +++ b/packages/example/src/routes/index.tsx @@ -10,16 +10,11 @@ const TodosStateLive = TodosState.Default("todos") export const Route = createFileRoute("/")({ component: Component.make(function* Index() { - const context = yield* Hook.useContext(TodosStateLive, { finalizerExecutionMode: "fork" }) - return yield* Effect.provide(Component.use(Todos, Todos => ), context) + return yield* Todos.pipe( + Effect.map(FC => ), + Effect.provide(yield* Hook.useContext(TodosStateLive, { finalizerExecutionMode: "fork" })), + ) }).pipe( Component.withRuntime(runtime.context) ) - - // component: Component.make("Index")( - // () => Hook.useContext(TodosStateLive, { finalizerExecutionMode: "fork" }), - // Effect.andThen(context => Effect.provide(Component.use(Todos, Todos => ), context)), - // ).pipe( - // Component.withRuntime(runtime.context) - // ) }) diff --git a/packages/example/src/todo/Todos.tsx b/packages/example/src/todo/Todos.tsx index 7529127..857f303 100644 --- a/packages/example/src/todo/Todos.tsx +++ b/packages/example/src/todo/Todos.tsx @@ -14,17 +14,17 @@ export class Todos extends Component.make(function* Todos() { Effect.addFinalizer(() => Console.log("Todos unmounted")), )) - const VTodo = yield* Component.useFC(Todo) + const TodoFC = yield* Todo return ( Todos - + {Chunk.map(todos, (v, k) => - + )} -- 2.49.1 From f867980913eaa38b2d57e7d31497ec0516f026d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 23 Jul 2025 14:13:25 +0200 Subject: [PATCH 24/28] Fix --- packages/effect-fc/src/Suspense.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/effect-fc/src/Suspense.ts b/packages/effect-fc/src/Suspense.ts index 3b20fac..7913fb8 100644 --- a/packages/effect-fc/src/Suspense.ts +++ b/packages/effect-fc/src/Suspense.ts @@ -49,7 +49,7 @@ export const isSuspense = (u: unknown): u is Suspense => Predicate.hasProperty(u export const suspense = , P extends {}>( self: T & Component.Component> ): ( - & T + & Omit, Component.Component.Context, P>> & Component.Component, Component.Component.Context, P & Suspense.Props> & Suspense ) => Object.setPrototypeOf( -- 2.49.1 From 0ac6fd2e06787068ed40f7e3ecfa98fa63691f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 23 Jul 2025 14:40:33 +0200 Subject: [PATCH 25/28] Fix --- packages/effect-fc/src/Component.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 236e895..be8bee7 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -59,12 +59,12 @@ const ComponentProto = Object.freeze({ }, []) }), - makeFunctionComponent( - this: Component, - runtimeRef: React.RefObject>, + makeFunctionComponent ( + this: Component, + runtimeRef: React.RefObject>>, scope: Scope.Scope, - ): React.FC { - return (props: any) => Runtime.runSync(runtimeRef.current)( + ): React.FC

{ + return (props: P) => Runtime.runSync(runtimeRef.current)( Effect.provideService(this.body(props), Scope.Scope, scope) ) }, -- 2.49.1 From dcb6cf30b21f7b7b80511938875a7e12a374570a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 23 Jul 2025 16:00:57 +0200 Subject: [PATCH 26/28] Refactoring --- packages/effect-fc/src/Component.ts | 71 ++++++++++++++--------------- packages/effect-fc/src/Suspense.ts | 8 ++-- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index be8bee7..bc10ac6 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -7,7 +7,7 @@ import * as Memoized from "./Memoized.js" export const TypeId: unique symbol = Symbol.for("effect-fc/Component") export type TypeId = typeof TypeId -export interface Component +export interface Component

extends Effect.Effect, never, Exclude>, Component.Options { new(_: never): {} readonly [TypeId]: TypeId @@ -18,9 +18,9 @@ extends Effect.Effect, never, Exclude>, Component.Op } export namespace Component { - export type Error = T extends Component ? E : never - export type Context = T extends Component ? R : never - export type Props = T extends Component ? P : never + export type Props = T extends Component ? P : never + export type Error = T extends Component ? E : never + export type Context = T extends Component ? R : never export interface Options { readonly displayName?: string @@ -34,7 +34,7 @@ const ComponentProto = Object.freeze({ ...Effectable.CommitPrototype, [TypeId]: TypeId, - commit: Effect.fnUntraced(function* (this: Component) { + commit: Effect.fnUntraced(function*

(this: Component) { const self = this const runtimeRef = React.useRef>>(null!) runtimeRef.current = yield* Effect.runtime>() @@ -59,8 +59,8 @@ const ComponentProto = Object.freeze({ }, []) }), - makeFunctionComponent ( - this: Component, + makeFunctionComponent

( + this: Component, runtimeRef: React.RefObject>>, scope: Scope.Scope, ): React.FC

{ @@ -77,17 +77,16 @@ const defaultOptions = { const nonReactiveTags = [Tracer.ParentSpan] as const - -export const isComponent = (u: unknown): u is Component => Predicate.hasProperty(u, TypeId) +export const isComponent = (u: unknown): u is Component<{}, unknown, unknown> => Predicate.hasProperty(u, TypeId) export namespace make { export type Gen = { >, P extends {} = {}>( body: (props: P) => Generator, ): Component< + P, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never, - P + [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never > >, A, B extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, @@ -99,7 +98,7 @@ export namespace make { >, props: NoInfer

, ) => B - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> >, A, B, C extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( @@ -111,7 +110,7 @@ export namespace make { props: NoInfer

, ) => B, b: (_: B, props: NoInfer

) => C, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> >, A, B, C, D extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( @@ -124,7 +123,7 @@ export namespace make { ) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> >, A, B, C, D, E extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( @@ -138,7 +137,7 @@ export namespace make { b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => E, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> >, A, B, C, D, E, F extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( @@ -153,7 +152,7 @@ export namespace make { c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => E, e: (_: E, props: NoInfer

) => F, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> >, A, B, C, D, E, F, G extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( @@ -169,7 +168,7 @@ export namespace make { d: (_: D, props: NoInfer

) => E, e: (_: E, props: NoInfer

) => F, f: (_: F, props: NoInfer

) => G, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> >, A, B, C, D, E, F, G, H extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( @@ -186,7 +185,7 @@ export namespace make { e: (_: E, props: NoInfer

) => F, f: (_: F, props: NoInfer

) => G, g: (_: G, props: NoInfer

) => H, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> >, A, B, C, D, E, F, G, H, I extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( @@ -204,7 +203,7 @@ export namespace make { f: (_: F, props: NoInfer

) => G, g: (_: G, props: NoInfer

) => H, h: (_: H, props: NoInfer

) => I, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> >, A, B, C, D, E, F, G, H, I, J extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( @@ -223,35 +222,35 @@ export namespace make { g: (_: G, props: NoInfer

) => H, h: (_: H, props: NoInfer

) => I, i: (_: I, props: NoInfer

) => J, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> } export type NonGen = { , P extends {} = {}>( body: (props: P) => Eff - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> , A, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => Eff, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> , A, B, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, b: (_: B, props: NoInfer

) => Eff, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> , A, B, C, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => Eff, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> , A, B, C, D, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => Eff, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> , A, B, C, D, E, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, @@ -259,7 +258,7 @@ export namespace make { c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => E, e: (_: E, props: NoInfer

) => Eff, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> , A, B, C, D, E, F, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, @@ -268,7 +267,7 @@ export namespace make { d: (_: D, props: NoInfer

) => E, e: (_: E, props: NoInfer

) => F, f: (_: F, props: NoInfer

) => Eff, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> , A, B, C, D, E, F, G, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, @@ -278,7 +277,7 @@ export namespace make { e: (_: E, props: NoInfer

) => F, f: (_: F, props: NoInfer

) => G, g: (_: G, props: NoInfer

) => Eff, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> , A, B, C, D, E, F, G, H, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, @@ -289,7 +288,7 @@ export namespace make { f: (_: F, props: NoInfer

) => G, g: (_: G, props: NoInfer

) => H, h: (_: H, props: NoInfer

) => Eff, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> , A, B, C, D, E, F, G, H, I, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, @@ -301,7 +300,7 @@ export namespace make { g: (_: G, props: NoInfer

) => H, h: (_: H, props: NoInfer

) => I, i: (_: I, props: NoInfer

) => Eff, - ): Component, Effect.Effect.Context, P> + ): Component, Effect.Effect.Context> } } @@ -367,15 +366,15 @@ export const withOptions: { )) export const withRuntime: { - ( +

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

- ( - self: Component, + ): (self: Component) => React.FC

+

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

-} = Function.dual(2, ( - self: Component, +} = Function.dual(2,

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

=> function WithRuntime(props) { const runtime = React.useContext(context) diff --git a/packages/effect-fc/src/Suspense.ts b/packages/effect-fc/src/Suspense.ts index 7913fb8..a8bbe58 100644 --- a/packages/effect-fc/src/Suspense.ts +++ b/packages/effect-fc/src/Suspense.ts @@ -46,11 +46,11 @@ const SuspenseProto = Object.freeze({ export const isSuspense = (u: unknown): u is Suspense => Predicate.hasProperty(u, TypeId) -export const suspense = , P extends {}>( - self: T & Component.Component> +export const suspense = , P extends {}>( + self: T & Component.Component, any, any> ): ( - & Omit, Component.Component.Context, P>> - & Component.Component, Component.Component.Context, P & Suspense.Props> + & Omit, Component.Component.Context>> + & Component.Component

, Component.Component.Context> & Suspense ) => Object.setPrototypeOf( Object.assign(function() {}, self, SuspenseProto), -- 2.49.1 From 8d393a85e90b53b6591af5ef9f4c0d83adfc3c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 23 Jul 2025 17:36:26 +0200 Subject: [PATCH 27/28] Fix --- packages/effect-fc/src/Component.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index bc10ac6..a2f0d21 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -34,7 +34,7 @@ const ComponentProto = Object.freeze({ ...Effectable.CommitPrototype, [TypeId]: TypeId, - commit: Effect.fnUntraced(function*

(this: Component) { + commit: Effect.fn("Component")(function*

(this: Component) { const self = this const runtimeRef = React.useRef>>(null!) runtimeRef.current = yield* Effect.runtime>() @@ -77,6 +77,7 @@ const defaultOptions = { const nonReactiveTags = [Tracer.ParentSpan] as const + export const isComponent = (u: unknown): u is Component<{}, unknown, unknown> => Predicate.hasProperty(u, TypeId) export namespace make { @@ -377,6 +378,8 @@ export const withRuntime: { self: Component, context: React.Context>, ): React.FC

=> function WithRuntime(props) { - const runtime = React.useContext(context) - return React.createElement(Runtime.runSync(runtime)(self), props) + return React.createElement( + Runtime.runSync(React.useContext(context))(self), + props, + ) }) -- 2.49.1 From 824b0b1ba5e1b136004c5af4fc070b3173cc4f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 23 Jul 2025 21:21:34 +0200 Subject: [PATCH 28/28] Version bump --- packages/effect-fc/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/effect-fc/package.json b/packages/effect-fc/package.json index ebc4908..8994f33 100644 --- a/packages/effect-fc/package.json +++ b/packages/effect-fc/package.json @@ -1,7 +1,7 @@ { "name": "effect-fc", "description": "Write React function components with Effect", - "version": "0.1.1", + "version": "0.1.2", "type": "module", "files": [ "./README.md", -- 2.49.1