@zeix/cause-effect
Version:
Cause & Effect - reactive state management primitives library for TypeScript.
141 lines (130 loc) • 3.69 kB
text/typescript
import {
validateCallback,
validateReadValue,
validateSignalValue,
} from '../errors'
import {
batchDepth,
type ComputedOptions,
DEFAULT_EQUALITY,
FLAG_DIRTY,
flush,
makeSubscribe,
type MemoCallback,
type MemoNode,
propagate,
refresh,
type SinkNode,
TYPE_MEMO,
} from '../graph'
import { isSignalOfType, isSyncFunction } from '../util'
/* === Types === */
/**
* A derived reactive computation that caches its result.
* Automatically tracks dependencies and recomputes when they change.
*
* @template T - The type of value computed by the memo
*/
type Memo<T extends {}> = {
readonly [Symbol.toStringTag]: 'Memo'
/**
* Gets the current value of the memo.
* Recomputes if dependencies have changed since last access.
* When called inside another reactive context, creates a dependency.
* @returns The computed value
*/
get(): T
}
/* === Exported Functions === */
/**
* Creates a derived reactive computation that caches its result.
* The computation automatically tracks dependencies and recomputes when they change.
* Uses lazy evaluation - only computes when the value is accessed.
*
* @since 0.18.0
* @template T - The type of value computed by the memo
* @param fn - The computation function that receives the previous value
* @param options - Optional configuration for the memo
* @param options.value - Optional initial value for reducer patterns
* @param options.equals - Optional equality function. Defaults to strict equality (`===`)
* @param options.guard - Optional type guard to validate values
* @param options.watched - Optional callback invoked when the memo is first watched by an effect.
* Receives an `invalidate` function to mark the memo dirty and trigger recomputation.
* Must return a cleanup function called when no effects are watching.
* @returns A Memo object with a get() method
*
* @example
* ```ts
* const count = createState(0);
* const doubled = createMemo(() => count.get() * 2);
* console.log(doubled.get()); // 0
* count.set(5);
* console.log(doubled.get()); // 10
* ```
*
* @example
* ```ts
* // Using previous value
* const sum = createMemo((prev) => prev + count.get(), { value: 0, equals: Object.is });
* ```
*/
function createMemo<T extends {}>(
fn: (prev: T) => T,
options: ComputedOptions<T> & { value: T },
): Memo<T>
function createMemo<T extends {}>(
fn: MemoCallback<T>,
options?: ComputedOptions<T>,
): Memo<T>
function createMemo<T extends {}>(
fn: MemoCallback<T>,
options?: ComputedOptions<T>,
): Memo<T> {
validateCallback(TYPE_MEMO, fn, isSyncFunction)
if (options?.value !== undefined)
validateSignalValue(TYPE_MEMO, options.value, options?.guard)
const node: MemoNode<T> = {
fn,
value: options?.value as T,
flags: FLAG_DIRTY,
sources: null,
sourcesTail: null,
sinks: null,
sinksTail: null,
equals: options?.equals ?? DEFAULT_EQUALITY,
error: undefined,
stop: undefined,
}
const watched = options?.watched
const subscribe = makeSubscribe(
node,
watched
? () =>
watched(() => {
propagate(node as unknown as SinkNode)
if (batchDepth === 0) flush()
})
: undefined,
)
return {
[Symbol.toStringTag]: TYPE_MEMO,
get() {
subscribe()
refresh(node as unknown as SinkNode)
if (node.error) throw node.error
validateReadValue(TYPE_MEMO, node.value)
return node.value
},
}
}
/**
* Checks if a value is a Memo signal.
*
* @since 0.18.0
* @param value - The value to check
* @returns True if the value is a Memo
*/
function isMemo<T extends {} = unknown & {}>(value: unknown): value is Memo<T> {
return isSignalOfType(value, TYPE_MEMO)
}
export { createMemo, isMemo, type Memo }