From 3be9d94aa8e251ba103c35f04da0afa56cb4770b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 22 Jan 2026 02:28:18 +0100 Subject: [PATCH] Add Component doc --- packages/effect-fc/src/Component.ts | 86 +++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 11 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index ee0d0a5..284f09d 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -47,16 +47,18 @@ export declare namespace Component { * Options that can be set on the component */ export interface Options { - /** Custom displayName for React DevTools and debugging */ + /** Custom displayName for React DevTools and debugging. */ readonly displayName?: string + /** - * Strategy used when executing finalizers on unmount/scope close + * Strategy used when executing finalizers on unmount/scope close. * @default ExecutionStrategy.sequential */ readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy + /** - * Debounce time before executing finalizers after component unmount - * Helps avoid unnecessary work during fast remount/remount cycles + * Debounce time before executing finalizers after component unmount. + * Helps avoid unnecessary work during fast remount/remount cycles. * @default "100 millis" */ readonly finalizerExecutionDebounce: Duration.DurationInput @@ -338,9 +340,17 @@ export declare namespace make { } /** - * Creates an Effect-based React component. + * Creates an Effect-FC Component following the same overloads and pipeline style as `Effect.fn`. * - * Follows the `Effect.fn` API. Supports both generator syntax (recommended) and direct Effect composition. + * This is the **recommended** way to define components. It supports: + * - Generator syntax (yield* style) — most ergonomic and readable + * - Direct Effect return (non-generator) + * - Chained transformation functions (like Effect.fn pipelines) + * - Optional tracing span with automatic `displayName` + * + * When you provide a `spanName` as the first argument, two things happen automatically: + * 1. A tracing span is created with that name (unless using `makeUntraced`) + * 2. The resulting React component gets `displayName = spanName` */ export const make: ( & make.Gen @@ -371,10 +381,15 @@ export const make: ( } /** - * Same as `make` but creates an untraced version (no automatic span created). - * Useful for very low-level utilities or when you want full control over tracing. + * Same as `make`, but creates an **untraced** version — no automatic tracing span is created. * - * Follows the `Effect.fnUntraced` API. + * Follows the exact same API shape as `Effect.fnUntraced`. + * Useful for: + * - Components where you want full manual control over tracing + * - Avoiding span noise in deeply nested UI + * + * When a string is provided as first argument, it is **only** used as the React component's `displayName` + * (no tracing span is created). */ export const makeUntraced: ( & make.Gen @@ -442,7 +457,7 @@ export const withOptions: { * * // Route * export const Route = createFileRoute("/")({ - * component: withRuntime(HomePage, runtime.context), + * component: Component.withRuntime(HomePage, runtime.context) * }) * ``` * @@ -468,6 +483,10 @@ export const withRuntime: { }) +/** + * Service that keeps track of scopes associated with React components + * (used internally by the `useScope` hook). + */ export class ScopeMap extends Effect.Service()("@effect-fc/Component/ScopeMap", { effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty())) }) {} @@ -487,6 +506,14 @@ export declare namespace useScope { } } +/** + * Hook that creates and manages a `Scope` for the current component instance. + * + * Automatically closes the scope whenever `deps` changes or the component unmounts. + * + * @param deps - dependency array like in `React.useEffect` + * @param options - finalizer execution control + */ export const useScope = Effect.fnUntraced(function*( deps: React.DependencyList, options?: useScope.Options, @@ -542,6 +569,9 @@ export const useScope = Effect.fnUntraced(function*( return scope }) +/** + * Runs an effect and returns its result only once on component mount. + */ export const useOnMount = Effect.fnUntraced(function* ( f: () => Effect.Effect ): Effect.fn.Return { @@ -553,6 +583,11 @@ export declare namespace useOnChange { export interface Options extends useScope.Options {} } +/** + * Runs an effect and returns its result whenever dependencies change. + * + * Provides its own `Scope` which closes whenever `deps` changes or the component unmounts. + */ export const useOnChange = Effect.fnUntraced(function* ( f: () => Effect.Effect, deps: React.DependencyList, @@ -574,6 +609,11 @@ export declare namespace useReactEffect { } } +/** + * Like `React.useEffect` but accepts an effect. + * + * Cleanup logic is handled through the `Scope` API rather than using imperative cleanup. + */ export const useReactEffect = Effect.fnUntraced(function* ( f: () => Effect.Effect, deps?: React.DependencyList, @@ -610,6 +650,11 @@ export declare namespace useReactLayoutEffect { export interface Options extends useReactEffect.Options {} } +/** + * Like `React.useReactLayoutEffect` but accepts an effect. + * + * Cleanup logic is handled through the `Scope` API rather than using imperative cleanup. + */ export const useReactLayoutEffect = Effect.fnUntraced(function* ( f: () => Effect.Effect, deps?: React.DependencyList, @@ -620,18 +665,27 @@ export const useReactLayoutEffect = Effect.fnUntraced(function* ( React.useLayoutEffect(() => runReactEffect(runtime, f, options), deps) }) +/** + * Get a synchronous run function for the current runtime context. + */ export const useRunSync = (): Effect.Effect< (effect: Effect.Effect) => A, never, Scope.Scope | R > => Effect.andThen(Effect.runtime(), Runtime.runSync) +/** + * Get a Promise-based run function for the current runtime context. + */ export const useRunPromise = (): Effect.Effect< (effect: Effect.Effect) => Promise, never, Scope.Scope | R > => Effect.andThen(Effect.runtime(), context => Runtime.runPromise(context)) +/** + * Turns a function returning an effect into a memoized synchronous function. + */ export const useCallbackSync = Effect.fnUntraced(function* ( f: (...args: Args) => Effect.Effect, deps: React.DependencyList, @@ -644,6 +698,9 @@ export const useCallbackSync = Effect.fnUntraced(function* Runtime.runSync(runtimeRef.current)(f(...args)), deps) }) +/** + * Turns a function returning an effect into a memoized Promise-based asynchronous function. + */ export const useCallbackPromise = Effect.fnUntraced(function* ( f: (...args: Args) => Effect.Effect, deps: React.DependencyList, @@ -660,10 +717,17 @@ export declare namespace useContext { export interface Options extends useOnChange.Options {} } +/** + * Hook that constructs a layer and returns the created context. + * + * The layer gets reconstructed everytime `layer` changes, so make sure its value is stable. + * + * Building a layer containing asynchronous effects require the component calling this hook to be made async using `Async.async`. + */ export const useContext = ( layer: Layer.Layer, options?: useContext.Options, -): Effect.Effect, E, Scope.Scope | RIn> => useOnChange(() => Effect.context().pipe( +): Effect.Effect, E, Exclude> => useOnChange(() => Effect.context().pipe( Effect.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))), Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)), Effect.andThen(runtime => runtime.runtimeEffect),