From 6c14495693424cde1869603af3ab883a8012e1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 25 Feb 2026 22:33:18 +0100 Subject: [PATCH] Improve comments --- packages/effect-fc/src/Component.ts | 433 ++++++++++++++++++++++++---- 1 file changed, 373 insertions(+), 60 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 8a21b5b..19e9726 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -8,11 +8,7 @@ export const TypeId: unique symbol = Symbol.for("@effect-fc/Component/Component" export type TypeId = typeof TypeId /** - * Interface representing an Effect-based React Component. - * - * This is both: - * - an Effect that produces a React function component - * - a constructor-like object with component metadata and options + * Represents an Effect-based React Component that integrates the Effect system with React. */ export interface Component

extends ComponentPrototype, ComponentOptions { @@ -98,18 +94,25 @@ const use = Effect.fnUntraced(function*

{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 + * const MyComponent = Component.make((props: { count: number }) => { + * return Effect.sync(() =>
{props.count}
) + * }) + * ``` */ export const make: ( & make.Gen @@ -393,15 +421,33 @@ export const make: ( } /** - * Same as `make`, but creates an **untraced** version — no automatic tracing span is created. + * Creates an Effect-FC Component without automatic distributed tracing. * - * 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 + * 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`. * - * When a string is provided as first argument, it is **only** used as the React component's `displayName` - * (no tracing span is created). + * 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 string is provided as the first argument, it is used **exclusively** as the React component's + * `displayName` for DevTools identification. No tracing span is created. + * + * @example + * ```tsx + * const MyComponent = Component.makeUntraced(function* (props: { count: number }) { + * const value = yield* someEffect + * return
{value}
+ * }) + * ``` + * + * @example With displayName only + * ```tsx + * const MyComponent = Component.makeUntraced("MyComponent", (props: { count: number }) => { + * return Effect.sync(() =>
{props.count}
) + * }) + * ``` */ export const makeUntraced: ( & make.Gen @@ -425,7 +471,21 @@ export const makeUntraced: ( ) /** - * Creates a new component with modified options while preserving original behavior. + * 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 MyComponent = Component.make(...) + * const MyComponentWithCustomOptions = MyComponent.pipe( + * Component.withOptions({ + * finalizerExecutionStrategy: ExecutionStrategy.parallel, + * finalizerExecutionDebounce: "50 millis" + * }) + * ) + * ``` */ export const withOptions: { >( @@ -444,21 +504,30 @@ export const withOptions: { )) /** - * Wraps an Effect-FC `Component` and turns it into a regular React function component - * that serves as an **entrypoint** into an Effect-FC component hierarchy. + * 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 the recommended way to connect Effect-FC components to the rest of your React app, - * especially when using routers (TanStack Router, React Router, etc.), lazy-loaded routes, - * or any place where a standard React component is expected. + * This is the recommended approach for integrating Effect-FC components 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 runtime is obtained from the provided React Context, allowing you to: - * - Provide dependencies once at a high level - * - Use the same runtime across an entire route tree or feature + * The Effect runtime is obtained from the provided React Context, enabling: + * - Single-point dependency injection at the application root + * - Consistent runtime environment across entire route trees or feature modules + * - Proper resource lifecycle management across component hierarchies * - * @example Using TanStack Router + * @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 - * // Main + * // Application root * export const runtime = ReactRuntime.make(Layer.empty) + * * function App() { * return ( * @@ -467,14 +536,12 @@ export const withOptions: { * ) * } * - * // Route + * // Route definition * export const Route = createFileRoute("/")({ * component: Component.withRuntime(HomePage, runtime.context) * }) * ``` * - * @param self - The Effect-FC Component you want to render as a regular React component. - * @param context - React Context that holds the Runtime to use for this component tree. See the `ReactRuntime` module to create one. */ export const withRuntime: {

( @@ -496,8 +563,10 @@ export const withRuntime: { /** - * Service that keeps track of scopes associated with React components - * (used internally by the `useScope` hook). + * 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 Effect.Service()("@effect-fc/Component/ScopeMap", { effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty())) @@ -519,13 +588,22 @@ export declare namespace useScope { } /** - * Hook that creates and manages a `Scope` for the current component instance. + * Effect hook that creates and manages a `Scope` for the current component instance. * - * Automatically closes the scope whenever `deps` changes or the component unmounts. + * This hook establishes a new scope that is automatically closed when: + * - The component unmounts + * - The dependency array `deps` changes * - * @param deps - dependency array like in `React.useEffect` - * @param options - finalizer execution control - */ + * 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, @@ -579,7 +657,23 @@ export const useScope = Effect.fnUntraced(function*( }) /** - * Runs an effect and returns its result only once on component mount. + * 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* (props) { + * const initialData = yield* useOnMount(() => fetchData) + * return

{initialData}
+ * }) + * ``` */ export const useOnMount = Effect.fnUntraced(function* ( f: () => Effect.Effect @@ -593,9 +687,33 @@ export declare namespace useOnChange { } /** - * Runs an effect and returns its result whenever dependencies change. + * Effect hook that executes an Effect whenever dependencies change and caches the result. * - * Provides its own `Scope` which closes whenever `deps` changes or the component unmounts. + * 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* useOnChange( + * fetchUser(props.userId), + * [props.userId], + * ) + * return
{userData.name}
+ * }) + * ``` */ export const useOnChange = Effect.fnUntraced(function* ( f: () => Effect.Effect, @@ -619,9 +737,35 @@ export declare namespace useReactEffect { } /** - * Like `React.useEffect` but accepts an effect. + * Effect hook that provides Effect-based semantics for React.useEffect. * - * Cleanup logic is handled through the `Scope` API rather than using imperative cleanup. + * This hook bridges React's useEffect with the Effect system, allowing you to use Effects + * for 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* useReactEffect( + * () => { + * const subscription = subscribe(props.id) + * return Effect.addFinalizer(() => subscription.unsubscribe()) + * }, + * [props.id] + * ) + * return
Subscribed to {props.id}
+ * }) + * ``` */ export const useReactEffect = Effect.fnUntraced(function* ( f: () => Effect.Effect, @@ -660,9 +804,43 @@ export declare namespace useReactLayoutEffect { } /** - * Like `React.useReactLayoutEffect` but accepts an effect. + * Effect hook that provides Effect-based semantics for React.useLayoutEffect. * - * Cleanup logic is handled through the `Scope` API rather than using imperative cleanup. + * 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* (props) { + * const ref = React.useRef(null) + * yield* useReactLayoutEffect( + * () => { + * if (ref.current) { + * const height = ref.current.offsetHeight + * // Perform layout-dependent operations + * } + * return Effect.void + * }, + * [] + * ) + * return
Content
+ * }) + * ``` */ export const useReactLayoutEffect = Effect.fnUntraced(function* ( f: () => Effect.Effect, @@ -675,7 +853,25 @@ export const useReactLayoutEffect = Effect.fnUntraced(function* ( }) /** - * Get a synchronous run function for the current runtime context. + * 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* (props) { + * const runSync = yield* useRunSync() + * const handleClick = () => { + * const result = runSync(someEffect) + * console.log(result) + * } + * return + * }) + * ``` */ export const useRunSync = (): Effect.Effect< (effect: Effect.Effect) => A, @@ -684,7 +880,29 @@ export const useRunSync = (): Effect.Effect< > => Effect.andThen(Effect.runtime(), Runtime.runSync) /** - * Get a Promise-based run function for the current runtime context. + * 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* (props) { + * const runPromise = yield* useRunPromise() + * const handleClick = async () => { + * try { + * const result = await runPromise(someEffect) + * console.log(result) + * } catch (error) { + * console.error(error) + * } + * } + * return + * }) + * ``` */ export const useRunPromise = (): Effect.Effect< (effect: Effect.Effect) => Promise, @@ -693,7 +911,31 @@ export const useRunPromise = (): Effect.Effect< > => Effect.andThen(Effect.runtime(), context => Runtime.runPromise(context)) /** - * Turns a function returning an effect into a memoized synchronous function. + * 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* useCallbackSync( + * (data: Data) => saveData(data), + * [props.onSave] + * ) + * return + * }) + * ``` */ export const useCallbackSync = Effect.fnUntraced(function* ( f: (...args: Args) => Effect.Effect, @@ -708,7 +950,41 @@ export const useCallbackSync = Effect.fnUntraced(function* void }) { + * const handleSave = yield* useCallbackPromise( + * (data: Data) => saveData(data), + * [props.onSave] + * ) + * return ( + * + * ) + * }) + * ``` */ export const useCallbackPromise = Effect.fnUntraced(function* ( f: (...args: Args) => Effect.Effect, @@ -727,11 +1003,48 @@ export declare namespace useContext { } /** - * Hook that constructs a layer and returns the created context. + * Effect hook that constructs an Effect Layer and returns the resulting context. * - * The layer gets reconstructed everytime `layer` changes, so make sure its value is stable. + * 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). * - * Building a layer containing asynchronous effects require the component calling this hook to be made async using `Async.async`. + * 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 MyComponent = Component.make(function* (props) { + * const context = yield* useContext( + * Layer.succeed(MyService, new MyServiceImpl()) + * ) + * // Use context to access services + * return
Using services
+ * }) + * ``` + * + * @example With async layer + * ```tsx + * const MyComponent = Component.make(function* (props) { + * const context = yield* useContext( + * Layer.effect(MyService, () => fetchServiceConfig()) + * ) + * return
Using async services
+ * }) + * + * // Must be wrapped with Async.async + * export default Async.async(MyComponent) + * ``` */ export const useContext = ( layer: Layer.Layer,