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] 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), )