diff --git a/packages/effect-fc/src/Async.ts b/packages/effect-fc/src/Async.ts index ad458eb..98ec0e9 100644 --- a/packages/effect-fc/src/Async.ts +++ b/packages/effect-fc/src/Async.ts @@ -4,25 +4,80 @@ import * as React from "react" import * as Component from "./Component.js" +/** + * A unique symbol representing the Async component type. + * Used as a type brand to identify Async components. + * + * @experimental + */ export const AsyncTypeId: unique symbol = Symbol.for("@effect-fc/Async/Async") -export type AsyncTypeId = typeof AsyncTypeId +/** + * The type of the Async type ID symbol. + */ +export type AsyncTypeId = typeof AsyncTypeId +/** + * An Async component that supports suspense and promise-based async operations. + * Combines Component behavior with Async-specific options. + * + * @example + * ```ts + * const MyAsyncComponent = async(component({ ... })) + * ``` + */ export interface Async extends AsyncPrototype, AsyncOptions {} + +/** + * The prototype object for Async components containing their methods and behaviors. + */ export interface AsyncPrototype { + /** + * The Async type ID brand. + */ readonly [AsyncTypeId]: AsyncTypeId } +/** + * Configuration options for Async components. + */ export interface AsyncOptions { + /** + * The default fallback React node to display while the async operation is pending. + * Used if no fallback is provided to the component when rendering. + */ readonly defaultFallback?: React.ReactNode } +/** + * Props for Async components, extending React.SuspenseProps without the children prop. + * The children are managed internally by the Async component. + */ export type AsyncProps = Omit +/** + * The prototype object for Async components. + * Provides the `asFunctionComponent` method for converting async components to React function components. + * + * @internal Use the `async` function to create Async components instead of accessing this directly. + */ export const AsyncPrototype: AsyncPrototype = Object.freeze({ [AsyncTypeId]: AsyncTypeId, + /** + * Converts an Async component to a React function component. + * + * @param runtimeRef - A reference to the Effect runtime for executing effects + * @returns A React function component that suspends while the async operation is executing + * + * @example + * ```ts + * const MyComponent = component({ ... }) + * const AsyncMyComponent = async(MyComponent) + * const FunctionComponent = AsyncMyComponent.asFunctionComponent(runtimeRef) + * ``` + */ asFunctionComponent

( this: Component.Component & Async, runtimeRef: React.RefObject>>, @@ -46,6 +101,16 @@ export const AsyncPrototype: AsyncPrototype = Object.freeze({ }, } as const) +/** + * An equivalence function for comparing AsyncProps that ignores the `fallback` property. + * Useful for memoization and re-render optimization. + * + * @param self - The first props object to compare + * @param that - The second props object to compare + * @returns `true` if the props are equivalent (excluding fallback), `false` otherwise + * + * @internal + */ export const defaultPropsEquivalence: Equivalence.Equivalence = ( self: Record, that: Record, @@ -71,8 +136,43 @@ export const defaultPropsEquivalence: Equivalence.Equivalence = ( } +/** + * A type guard to check if a value is an Async component. + * + * @param u - The value to check + * @returns `true` if the value is an Async component, `false` otherwise + * + * @example + * ```ts + * if (isAsync(component)) { + * // component is an Async component + * } + * ``` + */ export const isAsync = (u: unknown): u is Async => Predicate.hasProperty(u, AsyncTypeId) +/** + * Converts a Component into an Async component that supports suspense and promise-based async operations. + * + * The resulting component will wrap the original component with React.Suspense, + * allowing async Effect computations to suspend and resolve properly. + * + * Note: The component cannot have a prop named "promise" as it's reserved for internal use. + * + * @param self - The component to convert to an Async component + * @returns An Async component with the same body, error, and context types as the input + * + * @example + * ```ts + * const MyComponent = component({ + * body: (props) => // ... + * }) + * + * const AsyncMyComponent = async(MyComponent) + * ``` + * + * @throws Will produce a type error if the component has a "promise" prop + */ export const async = >( self: T & ( "promise" extends keyof Component.Component.Props @@ -96,6 +196,27 @@ export const async = >( )), ) +/** + * Applies options to an Async component, returning a new Async component with the updated configuration. + * + * Supports both curried and uncurried application styles. + * + * @param self - The Async component to apply options to (in uncurried form) + * @param options - The options to apply to the component + * @returns An Async component with the applied options + * + * @example + * ```ts + * const AsyncComponent = async(myComponent) + * + * // Uncurried + * const configured = withOptions(AsyncComponent, { defaultFallback: }) + * + * // Curried + * const configurer = withOptions({ defaultFallback: }) + * const configured = configurer(AsyncComponent) + * ``` + */ export const withOptions: { & Async>( options: Partial diff --git a/packages/effect-fc/src/Memoized.ts b/packages/effect-fc/src/Memoized.ts index 7176643..1e3edca 100644 --- a/packages/effect-fc/src/Memoized.ts +++ b/packages/effect-fc/src/Memoized.ts @@ -4,23 +4,80 @@ import * as React from "react" import type * as Component from "./Component.js" +/** + * A unique symbol representing the Memoized component type. + * Used as a type brand to identify Memoized components. + * + * @experimental + */ export const MemoizedTypeId: unique symbol = Symbol.for("@effect-fc/Memoized/Memoized") + +/** + * The type of the Memoized type ID symbol. + */ export type MemoizedTypeId = typeof MemoizedTypeId + +/** + * A Memoized component that uses React.memo to optimize re-renders based on prop equality. + * Combines Component behavior with Memoized-specific options. + * + * @template P The props type of the component + * + * @example + * ```ts + * const MyComponent = component({ ... }) + * const MemoizedComponent = memoized(MyComponent) + * ``` + */ export interface Memoized

extends MemoizedPrototype, MemoizedOptions

{} +/** + * The prototype object for Memoized components containing their methods and behaviors. + */ export interface MemoizedPrototype { + /** + * The Memoized type ID brand. + */ readonly [MemoizedTypeId]: MemoizedTypeId } +/** + * Configuration options for Memoized components. + * + * @template P The props type of the component + */ export interface MemoizedOptions

{ + /** + * An optional equivalence function for comparing component props. + * If provided, this function is used by React.memo to determine if props have changed. + * Returns `true` if props are equivalent (no re-render), `false` if they differ (re-render). + */ readonly propsEquivalence?: Equivalence.Equivalence

} +/** + * The prototype object for Memoized components. + * Provides the `transformFunctionComponent` method for memoizing React function components. + * + * @internal Use the `memoized` function to create Memoized components instead of accessing this directly. + */ export const MemoizedPrototype: MemoizedPrototype = Object.freeze({ [MemoizedTypeId]: MemoizedTypeId, + /** + * Transforms a React function component by wrapping it with React.memo. + * + * @param f - The React function component to memoize + * @returns A memoized version of the component that uses the configured propsEquivalence function + * + * @example + * ```ts + * const MemoizedComponent = memoized(MyComponent) + * const Fn = MemoizedComponent.transformFunctionComponent((props) =>

{props.x}
) + * ``` + */ transformFunctionComponent

( this: Memoized

, f: React.FC

, @@ -30,8 +87,39 @@ export const MemoizedPrototype: MemoizedPrototype = Object.freeze({ } as const) +/** + * A type guard to check if a value is a Memoized component. + * + * @param u - The value to check + * @returns `true` if the value is a Memoized component, `false` otherwise + * + * @example + * ```ts + * if (isMemoized(component)) { + * // component is a Memoized component + * } + * ``` + */ export const isMemoized = (u: unknown): u is Memoized => Predicate.hasProperty(u, MemoizedTypeId) +/** + * Converts a Component into a Memoized component that optimizes re-renders using React.memo. + * + * The resulting component will use React.memo to skip re-renders when props haven't changed, + * based on the configured equivalence function (or the default equality check). + * + * @param self - The component to convert to a Memoized component + * @returns A Memoized component with the same body, error, and context types as the input + * + * @example + * ```ts + * const MyComponent = component({ + * body: (props) => // ... + * }) + * + * const MemoizedComponent = memoized(MyComponent) + * ``` + */ export const memoized = >( self: T ): T & Memoized> => Object.setPrototypeOf( @@ -42,6 +130,29 @@ export const memoized = >( )), ) +/** + * Applies options to a Memoized component, returning a new Memoized component with the updated configuration. + * + * Supports both curried and uncurried application styles. + * + * @param self - The Memoized component to apply options to (in uncurried form) + * @param options - The options to apply to the component + * @returns A Memoized component with the applied options + * + * @example + * ```ts + * const MemoizedComponent = memoized(MyComponent) + * + * // Uncurried + * const configured = withOptions(MemoizedComponent, { + * propsEquivalence: (a, b) => a.id === b.id + * }) + * + * // Curried + * const configurer = withOptions({ propsEquivalence: (a, b) => a.id === b.id }) + * const configured = configurer(MemoizedComponent) + * ``` + */ export const withOptions: { & Memoized>( options: Partial>>