Files
effect-view/packages/effect-fc/src/Memoized.ts
T
Julien Valverdé 6917c72101
Lint / lint (push) Successful in 12s
Add comments
2026-03-10 03:18:45 +01:00

171 lines
5.3 KiB
TypeScript

/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
import { type Equivalence, Function, Predicate } from "effect"
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<P> extends MemoizedPrototype, MemoizedOptions<P> {}
/**
* 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<P> {
/**
* 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<P>
}
/**
* 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) => <div>{props.x}</div>)
* ```
*/
transformFunctionComponent<P extends {}>(
this: Memoized<P>,
f: React.FC<P>,
) {
return React.memo(f, this.propsEquivalence)
},
} 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<unknown> => 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 = <T extends Component.Component<any, any, any, any>>(
self: T
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf(
Object.assign(function() {}, self),
Object.freeze(Object.setPrototypeOf(
Object.assign({}, MemoizedPrototype),
Object.getPrototypeOf(self),
)),
)
/**
* 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: {
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
options: Partial<MemoizedOptions<Component.Component.Props<T>>>
): (self: T) => T
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
self: T,
options: Partial<MemoizedOptions<Component.Component.Props<T>>>,
): T
} = Function.dual(2, <T extends Component.Component<any, any, any, any> & Memoized<any>>(
self: T,
options: Partial<MemoizedOptions<Component.Component.Props<T>>>,
): T => Object.setPrototypeOf(
Object.assign(function() {}, self, options),
Object.getPrototypeOf(self),
))