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/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", diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 85331ac..a2f0d21 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -1,47 +1,93 @@ -import { Context, Effect, type Equivalence, ExecutionStrategy, Function, pipe, 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 type { ExcludeKeys } from "./utils.js" +import * as Memoized from "./Memoized.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 Effect.Effect, never, Exclude>, Component.Options { + new(_: never): {} + readonly [TypeId]: TypeId + /** @internal */ + makeFunctionComponent(runtimeRef: React.Ref>>, scope: Scope.Scope): React.FC

+ /** @internal */ readonly body: (props: P) => Effect.Effect - readonly displayName?: string - readonly options: Component.Options } 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 readonly finalizerExecutionMode: "sync" | "fork" readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy } } -const ComponentProto = Object.seal({ - pipe() { return Pipeable.pipeArguments(this, arguments) } +const ComponentProto = Object.freeze({ + ...Effectable.CommitPrototype, + [TypeId]: TypeId, + + commit: Effect.fn("Component")(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, + runtimeRef: React.RefObject>>, + scope: Scope.Scope, + ): React.FC

{ + return (props: P) => Runtime.runSync(runtimeRef.current)( + Effect.provideService(this.body(props), Scope.Scope, scope) + ) + }, } as const) -const defaultOptions: Component.Options = { +const defaultOptions = { finalizerExecutionMode: "sync", finalizerExecutionStrategy: ExecutionStrategy.sequential, -} +} as const const nonReactiveTags = [Tracer.ParentSpan] as const +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, @@ -53,7 +99,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: ( @@ -65,7 +111,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: ( @@ -78,7 +124,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: ( @@ -92,7 +138,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: ( @@ -107,7 +153,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: ( @@ -123,7 +169,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: ( @@ -140,7 +186,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: ( @@ -158,7 +204,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: ( @@ -177,35 +223,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, @@ -213,7 +259,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, @@ -222,7 +268,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, @@ -232,7 +278,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, @@ -243,7 +289,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, @@ -255,7 +301,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> } } @@ -266,52 +312,44 @@ 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({ - body: displayName - ? Effect.fn(displayName)(spanNameOrBody as any, ...pipeables as []) - : Effect.fn(spanNameOrBody as any, ...pipeables), - displayName, - options: { ...defaultOptions }, - }, ComponentProto) + return Object.setPrototypeOf( + Object.assign(function() {}, defaultOptions, { + 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.setPrototypeOf({ - body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []), - displayName: displayNameFromBody(body) ?? spanNameOrBody, - options: { ...defaultOptions }, - }, ComponentProto) + return (body: any, ...pipeables: any[]) => Object.setPrototypeOf( + Object.assign(function() {}, defaultOptions, { + 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.setPrototypeOf({ - body: Effect.fnUntraced(body as any, ...pipeables as []), - displayName: displayNameFromBody(body), - options: { ...defaultOptions }, -}, ComponentProto) +export const makeUntraced: make.Gen & make.NonGen = ( + body: Function, + ...pipeables: any[] +) => Object.setPrototypeOf( + Object.assign(function() {}, defaultOptions, { + body: Effect.fnUntraced(body as any, ...pipeables as []), + displayName: displayNameFromBody(body), + }), + ComponentProto, +) 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 @@ -324,148 +362,24 @@ export const withOptions: { self: T, options: Partial, ): T => Object.setPrototypeOf( - { ...self, options: { ...self.options, ...options } }, + Object.assign(function() {}, self, options), 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, + ): (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) - return React.createElement(Runtime.runSync(runtime)(useFC(self)), props) -}) - - -export interface Memoized

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

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

{ - readonly propsAreEqual?: Equivalence.Equivalence

- } -} - -export const memo = >( - self: ExcludeKeys>> -): T & Memoized> => Object.setPrototypeOf( - { ...self, memo: true, memoOptions: {} }, - Object.getPrototypeOf(self), -) - -export const memoWithEquivalence: { - >( - propsAreEqual: Equivalence.Equivalence> - ): ( - self: ExcludeKeys>> - ) => T & Memoized> - >( - self: ExcludeKeys>>, - propsAreEqual: Equivalence.Equivalence>, - ): T & Memoized> -} = Function.dual(2, >( - self: ExcludeKeys>>, - propsAreEqual: Equivalence.Equivalence>, -): T & Memoized> => Object.setPrototypeOf( - { ...self, memo: true, memoOptions: { propsAreEqual } }, - Object.getPrototypeOf(self), -)) - - -export interface Suspense { - readonly suspense: true -} - -export type SuspenseProps = Omit - -export const suspense = , P extends {}>( - self: ExcludeKeys & Component> -): T & Suspense => Object.setPrototypeOf( - { ...self, suspense: true }, - Object.getPrototypeOf(self), -) - - -export const useFC: { - ( - self: Component & Suspense - ): Effect.Effect, never, Exclude> - ( - self: Component - ): Effect.Effect, never, Exclude> -} = Effect.fn("useFC")(function* ( - self: Component & (Memoized

| Suspense | {}) -) { - 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.options, - )) - - const FC = React.useMemo(() => { - const f: React.FC

= Predicate.hasProperty(self, "suspense") - ? pipe( - function SuspenseInner(props: { readonly promise: Promise }) { - return React.use(props.promise) - }, - - SuspenseInner => ({ fallback, name, ...props }: P & SuspenseProps) => { - const promise = Runtime.runPromise(runtimeRef.current)( - Effect.provideService(self.body(props as P), Scope.Scope, scope) - ) - - return React.createElement( - React.Suspense, - { fallback, name }, - React.createElement(SuspenseInner, { promise }), - ) - }, - ) - : (props: P) => Runtime.runSync(runtimeRef.current)( - Effect.provideService(self.body(props), Scope.Scope, scope) - ) - - f.displayName = self.displayName ?? "Anonymous" - return Predicate.hasProperty(self, "memo") - ? React.memo(f, self.memoOptions.propsAreEqual) - : f - }, [scope]) - - return React.createElement(FC, props) - }, []) -}) - -export const use: { - ( - self: Component & Suspense, - fn: (Component: React.FC

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

) => React.ReactNode, - ): Effect.Effect> -} = Effect.fn("use")(function*(self, fn) { - return fn(yield* useFC(self)) + return React.createElement( + Runtime.runSync(React.useContext(context))(self), + props, + ) }) diff --git a/packages/effect-fc/src/Memoized.ts b/packages/effect-fc/src/Memoized.ts new file mode 100644 index 0000000..6358351 --- /dev/null +++ b/packages/effect-fc/src/Memoized.ts @@ -0,0 +1,47 @@ +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]: TypeId +} + +export namespace Memoized { + export interface Options

{ + readonly propsAreEqual?: Equivalence.Equivalence

+ } +} + + +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( + Object.assign(function() {}, self, MemoizedProto), + 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( + Object.assign(function() {}, 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..a8bbe58 --- /dev/null +++ b/packages/effect-fc/src/Suspense.ts @@ -0,0 +1,74 @@ +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" + + +export const TypeId: unique symbol = Symbol.for("effect-fc/Suspense") +export type TypeId = typeof TypeId + +export interface Suspense extends Suspense.Options { + readonly [TypeId]: TypeId +} + +export namespace Suspense { + export interface Options { + readonly defaultFallback?: React.ReactNode + } + + export type Props = Omit +} + + +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 {}>( + self: T & Component.Component, any, any> +): ( + & Omit, Component.Component.Context>> + & Component.Component

, Component.Component.Context> + & Suspense +) => Object.setPrototypeOf( + Object.assign(function() {}, self, SuspenseProto), + 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( + Object.assign(function() {}, 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" 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"!
-} diff --git a/packages/example/src/routes/dev/async-rendering.tsx b/packages/example/src/routes/dev/async-rendering.tsx index 7b123c9..52b9fb1 100644 --- a/packages/example/src/routes/dev/async-rendering.tsx +++ b/packages/example/src/routes/dev/async-rendering.tsx @@ -3,14 +3,14 @@ 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" // Generator version const RouteComponent = Component.make(function* AsyncRendering() { - const VMemoizedAsyncComponent = yield* Component.useFC(MemoizedAsyncComponent) - const VAsyncComponent = yield* Component.useFC(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...

, [])} /> + ) }).pipe( @@ -50,25 +50,28 @@ const RouteComponent = Component.make(function* AsyncRendering() { // ) -const AsyncComponent = Component.make(function* AsyncComponent() { - const VSubComponent = yield* Component.useFC(SubComponent) - yield* Effect.sleep("500 millis") +class AsyncComponent extends Component.make(function* AsyncComponent() { + const SubComponentFC = yield* SubComponent + + yield* Effect.sleep("500 millis") // Async operation + // Cannot use React hooks after the async operation return ( Rendered! - + ) }).pipe( - Component.suspense -) -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/routes/dev/memo.tsx b/packages/example/src/routes/dev/memo.tsx index f58e6ad..42f2f41 100644 --- a/packages/example/src/routes/dev/memo.tsx +++ b/packages/example/src/routes/dev/memo.tsx @@ -3,14 +3,11 @@ 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 } from "effect-fc" 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,20 +17,20 @@ const RouteComponent = Component.make(function* RouteComponent() { onChange={e => setValue(e.target.value)} /> - - + {yield* Effect.map(SubComponent, FC => )} + {yield* Effect.map(MemoizedSubComponent, FC => )} ) }).pipe( Component.withRuntime(runtime.context) ) -const SubComponent = Component.make(function* SubComponent() { +class SubComponent extends Component.make(function* SubComponent() { const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom)) return {id} -}) +}) {} -const MemoizedSubComponent = Component.memo(SubComponent) +class MemoizedSubComponent extends Memoized.memo(SubComponent) {} export const Route = createFileRoute("/dev/memo")({ component: RouteComponent, 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/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..857f303 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) @@ -14,19 +14,19 @@ export const Todos = 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) => - + )} ) -}) +}) {}