/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */ /** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */ import { Context, type Duration, Effect, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, identity, Layer, Option, Pipeable, Predicate, Ref, Runtime, Scope, Tracer } from "effect" import * as React from "react" export const ComponentTypeId: unique symbol = Symbol.for("@effect-fc/Component/Component") export type ComponentTypeId = typeof ComponentTypeId /** * Represents an Effect-based React Component that integrates the Effect system with React. */ export interface Component

extends ComponentPrototype, ComponentOptions { new(_: never): Record readonly [ComponentTypeId]: ComponentTypeId readonly "~Props": P readonly "~Success": A readonly "~Error": E readonly "~Context": R readonly "~Function": F readonly body: (props: P) => Effect.Effect } export declare namespace Component { export type Default

= Component> export type Any = Component export type Signature = (props: any) => React.ReactNode export type DefaultSignature

= (props: P) => A export type Props = [T] extends [Component] ? P : never export type Success = [T] extends [Component] ? A : never export type Error = [T] extends [Component] ? E : never export type Context = [T] extends [Component] ? R : never export type Function = [T] extends [Component] ? F : never export type AsComponent = Component, Success, Error, Context, Function> } export interface ComponentImpl

extends Component, ComponentImplPrototype {} export interface ComponentImplPrototype { readonly use: Effect.Effect> asFunctionComponent(contextRef: React.Ref>>): F setFunctionComponentName(f: F): void transformFunctionComponent(f: F): F } export const ComponentImplPrototype: ComponentImplPrototype = Object.freeze({ get use() { return use(this) }, asFunctionComponent

( this: ComponentImpl, contextRef: React.RefObject>>, ) { return (props: P) => Effect.runSyncWith(contextRef.current)( Effect.andThen( useScope([], this), scope => Effect.provideService(this.body(props), Scope.Scope, scope), ) ) }, setFunctionComponentName

( this: ComponentImpl, f: React.FC

, ) { f.displayName = this.displayName ?? "Anonymous" }, transformFunctionComponent: identity, } as const) const use = Effect.fnUntraced(function*

( self: ComponentImpl ) { // biome-ignore lint/style/noNonNullAssertion: React ref initialization const contextRef = React.useRef>>(null!) contextRef.current = yield* Effect.context>() return yield* React.useState(() => Effect.runSyncWith(contextRef.current)(Effect.cachedFunction( (_services: readonly any[]) => Effect.sync(() => { const f = self.asFunctionComponent(contextRef) self.setFunctionComponentName(f) return self.transformFunctionComponent(f) }), Equivalence.array(Equivalence.strictEqual()), )))[0](Array.from( Context.omit(...self.nonReactiveTags)(contextRef.current).mapUnsafe.values() )) }) export interface ComponentPrototype extends Pipeable.Pipeable { readonly [ComponentTypeId]: ComponentTypeId readonly use: Effect.Effect> } export const ComponentPrototype: ComponentPrototype = Object.freeze( Object.defineProperties( { [ComponentTypeId]: ComponentTypeId, ...Pipeable.Prototype, }, Object.getOwnPropertyDescriptors(ComponentImplPrototype), ) as ComponentPrototype ) export interface ComponentOptions { /** * Custom display name for the component in React DevTools and debugging utilities. */ readonly displayName?: string /** * Context tags that should not trigger component remount when their values change. * * @default [Tracer.ParentSpan] */ readonly nonReactiveTags: readonly Context.Key[] /** * Specifies the execution strategy for finalizers when the component unmounts or its scope closes. * Determines whether finalizers execute sequentially or in parallel. * * @default ExecutionStrategy.sequential */ readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy /** * Debounce duration before executing finalizers after component unmount. * Prevents unnecessary cleanup work during rapid remount/unmount cycles, * which is common in development and certain UI patterns. * * @default "100 millis" */ readonly finalizerExecutionDebounce: Duration.Input } export const defaultOptions: ComponentOptions = { nonReactiveTags: [Tracer.ParentSpan], finalizerExecutionStrategy: ExecutionStrategy.sequential, finalizerExecutionDebounce: "100 millis", } export const isComponent = (u: unknown): u is Component.Default<{}, React.ReactNode, unknown, unknown> => Predicate.hasProperty(u, ComponentTypeId) export declare namespace make { export type Gen = { , A extends React.ReactNode, P extends {} = {}>( body: (props: P) => Generator ): Component.Default< P, A, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? R : never > , A, B extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? R : never >, props: NoInfer

, ) => B, ): Component.Default, Effect.Error, Effect.Services> , A, B, C extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? R : never >, props: NoInfer

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

) => C, ): Component.Default, Effect.Error, Effect.Services> , A, B, C, D extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? R : never >, props: NoInfer

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

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

) => D, ): Component.Default, Effect.Error, Effect.Services> , A, B, C, D, E extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? R : never >, props: NoInfer

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

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

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

) => E, ): Component.Default, Effect.Error, Effect.Services> , A, B, C, D, E, F extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? R : never >, props: NoInfer

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

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

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

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

) => F, ): Component.Default, Effect.Error, Effect.Services> , A, B, C, D, E, F, G extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? R : never >, props: NoInfer

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

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

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

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

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

) => G, ): Component.Default, Effect.Error, Effect.Services> , A, B, C, D, E, F, G, H extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? R : never >, props: NoInfer

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

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

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

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

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

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

) => H, ): Component.Default, Effect.Error, Effect.Services> , A, B, C, D, E, F, G, H, I extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? R : never >, props: NoInfer

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

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

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

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

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

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

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

) => I, ): Component.Default, Effect.Error, Effect.Services> , A, B, C, D, E, F, G, H, I, J extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect] ? R : never >, props: NoInfer

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

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

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

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

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

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

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

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

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

) => Eff, ): Component.Default, Effect.Error, Effect.Services> , A, B, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

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

) => Eff, ): Component.Default, Effect.Error, Effect.Services> , 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.Default, Effect.Error, Effect.Services> , 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.Default, Effect.Error, Effect.Services> , A, B, C, D, E, 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

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

) => Eff, ): Component.Default, Effect.Error, Effect.Services> , A, B, C, D, E, F, 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

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

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

) => Eff, ): Component.Default, Effect.Error, Effect.Services> , A, B, C, D, E, F, G, 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

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

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

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

) => Eff, ): Component.Default, Effect.Error, Effect.Services> , A, B, C, D, E, F, G, H, 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

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

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

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

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

) => Eff, ): Component.Default, Effect.Error, Effect.Services> , A, B, C, D, E, F, G, H, I, 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

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

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

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

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

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

) => Eff, ): Component.Default, Effect.Error, Effect.Services> } } /** * Creates an Effect-FC Component using the same overloads and pipeline composition style as `Effect.fn`. * * This is the **recommended** approach for defining Effect-FC components. It provides comprehensive * support for multiple component definition patterns: * * - **Generator syntax** (yield* style): Most ergonomic and readable approach for sequential operations * - **Direct Effect return**: For simple components that return an Effect directly * - **Chained transformation functions**: Enables Effect.fn-style pipelines for composable transformations * - **Automatic tracing**: Optional tracing span creation with automatic `displayName` assignment * * When a `spanName` string is provided, the following occurs automatically: * 1. A distributed tracing span is created with the specified name * 2. The resulting React component receives `displayName = spanName` for DevTools visibility * * @example * ```tsx * const MyComponent = Component.make("MyComponent")(function* (props: { count: number }) { * const value = yield* someEffect * return

{value}
* }) * ``` * * @example As an opaque type using class syntax * ```tsx * class MyComponent extends Component.make("MyComponent")(function* (props: { count: number }) { * const value = yield* someEffect * return
{value}
* }) {} * ``` * * @example Without name * ```tsx * class MyComponent extends Component.make(function* (props: { count: number }) { * const value = yield* someEffect * return
{value}
* }) {} * ``` * * @example Using pipeline * ```tsx * class MyComponent extends Component.make("MyComponent")( * (props: { count: number }) => someEffect, * Effect.map(value =>
{value}
), * ) {} * ``` */ export const make: ( & make.Gen & make.NonGen & (( spanName: string, spanOptions?: Tracer.SpanOptions, ) => make.Gen & make.NonGen) ) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => { if (typeof spanNameOrBody !== "string") { return Object.setPrototypeOf( Object.assign(function() {}, defaultOptions, { body: Effect.fn(spanNameOrBody as any, ...pipeables), }), ComponentPrototype, ) } else { const spanOptions = pipeables[0] return (body: any, ...pipeables: any[]) => Object.setPrototypeOf( Object.assign(function() {}, defaultOptions, { body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []), displayName: spanNameOrBody, }), ComponentPrototype, ) } } /** * Creates an Effect-FC Component without automatic distributed tracing. * * This function provides the same API surface as `make`, but does not create automatic tracing spans. * It follows the exact same overload structure as `Effect.fnUntraced`. * * Use this variant when you need: * - Full manual control over tracing instrumentation * - To reduce tracing overhead in deeply nested component hierarchies * - To avoid span noise in performance-sensitive applications * * When a `spanName` string is provided, it is used **exclusively** as the React component's * `displayName` for DevTools identification. No tracing span is created. * * @example * ```tsx * const MyComponent = Component.makeUntraced("MyComponent")(function* (props: { count: number }) { * const value = yield* someEffect * return
{value}
* }) * ``` * * @example As an opaque type using class syntax * ```tsx * class MyComponent extends Component.makeUntraced("MyComponent")(function* (props: { count: number }) { * const value = yield* someEffect * return
{value}
* }) {} * ``` * * @example Without name * ```tsx * class MyComponent extends Component.makeUntraced(function* (props: { count: number }) { * const value = yield* someEffect * return
{value}
* }) {} * ``` * * @example Using pipeline * ```tsx * class MyComponent extends Component.makeUntraced("MyComponent")( * (props: { count: number }) => someEffect, * Effect.map(value =>
{value}
), * ) {} * ``` */ export const makeUntraced: ( & make.Gen & make.NonGen & ((name: string) => make.Gen & make.NonGen) ) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => ( typeof spanNameOrBody !== "string" ? Object.setPrototypeOf( Object.assign(function() {}, defaultOptions, { body: Effect.fnUntraced(spanNameOrBody as any, ...pipeables as []), }), ComponentPrototype, ) : (body: any, ...pipeables: any[]) => Object.setPrototypeOf( Object.assign(function() {}, defaultOptions, { body: Effect.fnUntraced(body, ...pipeables as []), displayName: spanNameOrBody, }), ComponentPrototype, ) ) export declare namespace withSignature { export type Result = ( & Omit> & Component, Component.Success, Component.Error, Component.Context, F> ) } export const withSignature: { (): ( self: T ) => withSignature.Result ( self: T ): withSignature.Result } = (self?: Component.Any): any => self === undefined ? identity : self /** * Creates a new component with modified configuration options while preserving all original behavior. * * This function allows you to customize component-level options such as finalizer execution strategy * and debounce timing. * * @example * ```tsx * const MyComponentWithCustomOptions = MyComponent.pipe( * Component.withOptions({ * finalizerExecutionStrategy: ExecutionStrategy.parallel, * finalizerExecutionDebounce: "50 millis", * }) * ) * ``` */ export const withOptions: { ( options: Partial ): (self: T) => T ( self: T, options: Partial, ): T } = Function.dual(2, ( self: T, options: Partial, ): T => Object.setPrototypeOf( Object.assign(function() {}, self, options), Object.getPrototypeOf(self), )) /** * Wraps an Effect-FC Component and converts it into a standard React function component, * serving as an **entrypoint** into an Effect-FC component hierarchy. * * This is how Effect-FC components are integrated with the broader React ecosystem, * particularly when: * - Using client-side routers (TanStack Router, React Router, etc.) * - Implementing lazy-loaded or code-split routes * - Connecting to third-party libraries expecting standard React components * - Creating component boundaries between Effect-FC and non-Effect-FC code * * The Effect runtime is obtained from the provided React Context. * * @param self - The Effect-FC Component to be rendered as a standard React component * @param context - React Context providing the Effect Runtime for this component tree. * Create this using the `ReactRuntime` module. * * @example Integration with TanStack Router * ```tsx * // Application root * export const runtime = ReactRuntime.make(Layer.empty) * * function App() { * return ( * * * * ) * } * * // Route definition * export const Route = createFileRoute("/")({ * component: Component.withRuntime(HomePage, runtime.context) * }) * ``` * */ export const withRuntime: {

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

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

( self: Component, context: React.Context>, ) => function WithRuntime(props: P) { return React.createElement( Runtime.runSync(React.useContext(context))(self.use) as React.FC

, props, ) }) /** * Internal Effect service that maintains a registry of scopes associated with React component instances. * * This service is used internally by the `useScope` hook to manage the lifecycle of component scopes, * including tracking active scopes and coordinating their cleanup when components unmount or dependencies change. */ export class ScopeMap extends Context.Service> }>()( "@effect-fc/Component/ScopeMap" ) { static readonly layer = Layer.effect(ScopeMap, Effect.map( Ref.make(HashMap.empty()), ref => ({ ref }), )) } export declare namespace ScopeMap { export interface Entry { readonly scope: Scope.Closeable readonly closeFiber: Option.Option> } } export declare namespace useScope { export interface Options { readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy readonly finalizerExecutionDebounce?: Duration.Input } } /** * Effect hook that creates and manages a `Scope` for the current component instance. * * This hook establishes a new scope that is automatically closed when: * - The component unmounts * - The dependency array `deps` changes * * The scope provides a resource management boundary for any Effects executed within the component, * ensuring proper cleanup of resources and execution of finalizers. * * @param deps - Dependency array following React.useEffect semantics. The scope is recreated * whenever any dependency changes. * @param options - Configuration for finalizer execution behavior, including execution strategy * and debounce timing. * * @returns An Effect that produces a `Scope` for resource management */ export const useScope = Effect.fnUntraced(function*( deps: React.DependencyList, options?: useScope.Options, ): Effect.fn.Return { // biome-ignore lint/style/noNonNullAssertion: context initialization const contextRef = React.useRef>(null!) contextRef.current = yield* Effect.context() const { key, scope } = React.useMemo(() => Effect.runSyncWith(contextRef.current)(Effect.Do.pipe( Effect.bind("scopeMapRef", () => Effect.map( ScopeMap as unknown as Effect.Effect, scopeMap => scopeMap.ref, )), Effect.let("key", () => ({})), Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy)), Effect.tap(({ scopeMapRef, key, scope }) => Ref.update(scopeMapRef, HashMap.set(key, { scope, closeFiber: Option.none(), })) ), // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList )), deps) // biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "key" React.useEffect(() => Effect.runSyncWith(contextRef.current)( (ScopeMap as unknown as Effect.Effect).pipe( Effect.map(scopeMap => scopeMap.ref), Effect.tap(ref => Ref.get(ref).pipe( Effect.flatMap(map => Effect.fromOption(HashMap.get(map, key))), Effect.flatMap(entry => Option.match(entry.closeFiber, { onSome: fiber => Effect.forkDetach(Fiber.interrupt(fiber)), onNone: () => Effect.void, })), )), Effect.map(ref => () => Effect.runSyncWith(contextRef.current)(Effect.flatMap( Effect.sleep(options?.finalizerExecutionDebounce ?? defaultOptions.finalizerExecutionDebounce).pipe( Effect.andThen(Scope.close(scope, Exit.void)), Effect.onExit(() => Ref.update(ref, HashMap.remove(key))), Effect.forkDetach, ), fiber => Ref.update(ref, HashMap.set(key, { scope, closeFiber: Option.some(fiber), })), )) ), ) ), [key]) return scope }) /** * Effect hook that executes an Effect once when the component mounts and caches the result. * * This hook is useful for one-time initialization logic that should not be re-executed * when the component re-renders. The Effect is executed exactly once during the component's * initial mount, and the cached result is returned on all subsequent renders. * * @param f - A function that returns the Effect to execute on mount * * @returns An Effect that produces the cached result of the Effect * * @example * ```tsx * const MyComponent = Component.make(function*() { * const initialData = yield* Component.useOnMount(() => getData) * return

{initialData}
* }) * ``` */ export const useOnMount = Effect.fnUntraced(function* ( f: () => Effect.Effect ): Effect.fn.Return { const context = yield* Effect.context() return yield* React.useState(() => Effect.runSyncWith(context)(Effect.cached(f())))[0] }) export declare namespace useOnChange { export interface Options extends useScope.Options {} } /** * Effect hook that executes an Effect whenever dependencies change and caches the result. * * This hook combines the dependency-tracking behavior of React.useEffect with Effect caching. * The Effect is re-executed whenever any dependency in the `deps` array changes, and the result * is cached until the next dependency change. * * A dedicated scope is created for each dependency change, ensuring proper resource cleanup: * - The scope closes when dependencies change * - The scope closes when the component unmounts * - All finalizers are executed according to the configured execution strategy * * @param f - A function that returns the Effect to execute * @param deps - Dependency array following React.useEffect semantics * @param options - Configuration for scope and finalizer behavior * * @returns An Effect that produces the cached result of the Effect * * @example * ```tsx * const MyComponent = Component.make(function* (props: { userId: string }) { * const userData = yield* Component.useOnChange( * getUser(props.userId), * [props.userId], * ) * return
{userData.name}
* }) * ``` */ export const useOnChange = Effect.fnUntraced(function* ( f: () => Effect.Effect, deps: React.DependencyList, options?: useOnChange.Options, ): Effect.fn.Return> { const context = yield* Effect.context>() const scope = yield* useScope(deps, options) // biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "scope" return yield* React.useMemo(() => Effect.runSyncWith(context)( Effect.cached(Effect.provideService(f(), Scope.Scope, scope)) ), [scope]) }) export declare namespace useReactEffect { export interface Options { readonly finalizerExecutionMode?: "sync" | "fork" readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy } } /** * Effect hook that provides Effect-based semantics for React.useEffect. * * This hook bridges React's useEffect with the Effect system, allowing you to use Effects * for React side effects while maintaining React's dependency tracking and lifecycle semantics. * * Unlike React.useEffect which uses imperative cleanup functions, this hook leverages the * Effect Scope API for resource management. Cleanup logic is expressed declaratively through * finalizers registered with the scope, providing better composability and error handling. * * @param f - A function that returns an Effect to execute as a side effect * @param deps - Optional dependency array following React.useEffect semantics. * If omitted, the effect runs after every render. * @param options - Configuration for finalizer execution mode (sync or fork) and strategy * * @returns An Effect that produces void * * @example * ```tsx * const MyComponent = Component.make(function* (props: { id: string }) { * yield* Component.useReactEffect( * () => getNotificationStreamForUser(props.id).pipe( * Stream.unwrap, * Stream.runForEach(notification => Console.log(`Notification received: ${ notification }`), * Effect.forkScoped, * ), * [props.id], * ) * return
Subscribed to notifications for {props.id}
* }) * ``` */ export const useReactEffect = Effect.fnUntraced(function* ( f: () => Effect.Effect, deps?: React.DependencyList, options?: useReactEffect.Options, ): Effect.fn.Return> { const context = yield* Effect.context>() // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList React.useEffect(() => runReactEffect(context, f, options), deps) }) const runReactEffect = ( context: Context.Context>, f: () => Effect.Effect, options?: useReactEffect.Options, ) => Effect.Do.pipe( Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy)), Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(f(), Scope.Scope, scope))), Effect.map(({ scope }) => () => { switch (options?.finalizerExecutionMode ?? "fork") { case "sync": Effect.runSyncWith(context)(Scope.close(scope, Exit.void)) break case "fork": Effect.runForkWith(context)(Scope.close(scope, Exit.void)) break } } ), Effect.runSyncWith(context), ) export declare namespace useReactLayoutEffect { export interface Options extends useReactEffect.Options {} } /** * Effect hook that provides Effect-based semantics for React.useLayoutEffect. * * This hook is identical to `useReactEffect` but executes synchronously after DOM mutations * but before the browser paints, following React.useLayoutEffect semantics. * * Use this hook when you need to: * - Measure DOM elements (e.g., for layout calculations) * - Synchronously update state based on DOM measurements * - Avoid visual flicker from asynchronous updates * * Like `useReactEffect`, cleanup logic is handled through the Effect Scope API rather than * imperative cleanup functions, providing declarative and composable resource management. * * @param f - A function that returns an Effect to execute as a layout side effect * @param deps - Optional dependency array following React.useLayoutEffect semantics. * If omitted, the effect runs after every render. * @param options - Configuration for finalizer execution mode (sync or fork) and strategy * * @returns An Effect that produces void * * @example * ```tsx * const MyComponent = Component.make(function*() { * const ref = React.useRef(null) * yield* Component.useReactLayoutEffect( * () => Effect.gen(function* () { * const element = ref.current * if (element) { * const rect = element.getBoundingClientRect() * yield* Console.log(`Element dimensions: ${ rect.width }x${ rect.height }`) * } * }), * [], * ) * return
Content
* }) * ``` */ export const useReactLayoutEffect = Effect.fnUntraced(function* ( f: () => Effect.Effect, deps?: React.DependencyList, options?: useReactLayoutEffect.Options, ): Effect.fn.Return> { const context = yield* Effect.context>() // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList React.useLayoutEffect(() => runReactEffect(context, f, options), deps) }) /** * Effect hook that provides a synchronous function to execute Effects within the current runtime context. * * This hook returns a function that can execute Effects synchronously, blocking until completion. * Use this when you need to run Effects from non-Effect code (e.g., event handlers, callbacks) * within a component. * * @returns An Effect that produces a function capable of synchronously executing Effects * * @example * ```tsx * const MyComponent = Component.make(function*() { * const runSync = yield* Component.useRunSync() // Specify required services * const runSync = yield* Component.useRunSync() // Or no service requirements * * return * }) * ``` */ export const useRunSync = (): Effect.Effect< (effect: Effect.Effect) => A, never, Scope.Scope | R > => Effect.map(Effect.context(), Effect.runSyncWith) /** * Effect hook that provides an asynchronous function to execute Effects within the current runtime context. * * This hook returns a function that executes Effects asynchronously, returning a Promise that resolves * with the Effect's result. Use this when you need to run Effects from non-Effect code (e.g., event handlers, * async callbacks) and want to handle the result asynchronously. * * @returns An Effect that produces a function capable of asynchronously executing Effects * * @example * ```tsx * const MyComponent = Component.make(function*() { * const runPromise = yield* Component.useRunPromise() // Specify required services * const runPromise = yield* Component.useRunPromise() // Or no service requirements * * return * }) * ``` */ export const useRunPromise = (): Effect.Effect< (effect: Effect.Effect) => Promise, never, Scope.Scope | R > => Effect.map(Effect.context(), Effect.runPromiseWith) /** * Effect hook that memoizes a function that returns an Effect, providing synchronous execution. * * This hook wraps a function that returns an Effect and returns a memoized version that: * - Executes the Effect synchronously when called * - Is memoized based on the provided dependency array * - Maintains referential equality across renders when dependencies don't change * * Use this to create stable callback references for event handlers and other scenarios * where you need to execute Effects synchronously from non-Effect code. * * @param f - A function that accepts arguments and returns an Effect * @param deps - Dependency array. The memoized function is recreated when dependencies change. * * @returns An Effect that produces a memoized function with the same signature as `f` * * @example * ```tsx * const MyComponent = Component.make(function* (props: { onSave: (data: Data) => void }) { * const handleSave = yield* Component.useCallbackSync( * (data: Data) => Effect.sync(() => props.onSave(data)), * [props.onSave], * ) * * return * }) * ``` */ export const useCallbackSync = Effect.fnUntraced(function* ( f: (...args: Args) => Effect.Effect, deps: React.DependencyList, ): Effect.fn.Return<(...args: Args) => A, never, R> { // biome-ignore lint/style/noNonNullAssertion: context initialization const contextRef = React.useRef>(null!) contextRef.current = yield* Effect.context() // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList return React.useCallback((...args: Args) => Effect.runSyncWith(contextRef.current)(f(...args)), deps) }) /** * Effect hook that memoizes a function that returns an Effect, providing asynchronous execution. * * This hook wraps a function that returns an Effect and returns a memoized version that: * - Executes the Effect asynchronously when called, returning a Promise * - Is memoized based on the provided dependency array * - Maintains referential equality across renders when dependencies don't change * * Use this to create stable callback references for async event handlers and other scenarios * where you need to execute Effects asynchronously from non-Effect code. * * @param f - A function that accepts arguments and returns an Effect * @param deps - Dependency array. The memoized function is recreated when dependencies change. * * @returns An Effect that produces a memoized function that returns a Promise * * @example * ```tsx * const MyComponent = Component.make(function* (props: { onSave: (data: Data) => void }) { * const handleSave = yield* Component.useCallbackPromise( * (data: Data) => Effect.promise(() => props.onSave(data)), * [props.onSave], * ) * * return * }) * ``` */ export const useCallbackPromise = Effect.fnUntraced(function* ( f: (...args: Args) => Effect.Effect, deps: React.DependencyList, ): Effect.fn.Return<(...args: Args) => Promise, never, R> { // biome-ignore lint/style/noNonNullAssertion: context initialization const contextRef = React.useRef>(null!) contextRef.current = yield* Effect.context() // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList return React.useCallback((...args: Args) => Effect.runPromiseWith(contextRef.current)(f(...args)), deps) }) export declare namespace useContext { export interface Options extends useOnChange.Options {} } /** * Effect hook that constructs an Effect Layer and returns the resulting context. * * This hook creates a managed runtime from the provided layer and returns the context it produces. * The layer is reconstructed whenever its value changes, so ensure the layer reference is stable * (typically by memoizing it or defining it outside the component). * * The hook automatically manages the layer's lifecycle: * - The layer is built when the component mounts or when the layer reference changes * - Resources are properly released when the component unmounts or dependencies change * - Finalizers are executed according to the configured execution strategy * * @param layer - The Effect Layer to construct. Should be a stable reference to avoid unnecessary * reconstruction. Consider memoizing with React.useMemo if defined inline. * @param options - Configuration for scope and finalizer behavior * * @returns An Effect that produces the context created by the layer * * @throws If the layer contains asynchronous effects, the component must be wrapped with `Async.async` * * @example * ```tsx * const MyLayer = Layer.succeed(MyService, new MyServiceImpl()) * const MyComponent = Component.make(function*() { * const context = yield* Component.useContextFromLayer(MyLayer) * const Sub = yield* SubComponent.use.pipe( * Effect.provide(context) * ) * * return * }) * ``` * * @example With memoized layer * ```tsx * const MyComponent = Component.make(function*(props: { id: string })) { * const context = yield* Component.useContextFromLayer( * React.useMemo(() => Layer.succeed(MyService, new MyServiceImpl(props.id)), [props.id]) * ) * const Sub = yield* SubComponent.use.pipe( * Effect.provide(context) * ) * * return * }) * ``` * * @example With async layer * ```tsx * const MyAsyncLayer = Layer.effect(MyService, someAsyncEffect) * const MyComponent = Component.make(function*() { * const context = yield* Component.useContextFromLayer(MyAsyncLayer) * const Sub = yield* SubComponent.use.pipe( * Effect.provide(context) * ) * * return * }).pipe( * Async.async // Required to handle async layer effects * ) */ export const useContextFromLayer = ( layer: Layer.Layer, options?: useContext.Options, ): Effect.Effect, E, RIn | Scope.Scope> => useOnChange(() => Effect.flatMap( Effect.context(), context => Layer.build(Layer.provide(layer, Layer.succeedContext(context))), ), [layer], options)