react-rx-signals
Version:
SolidJS-style signals and stores for React using RxJS - 98% fewer re-renders with fine-grained reactivity
595 lines (579 loc) • 23 kB
TypeScript
import { Observable } from 'rxjs';
import React, { DependencyList } from 'react';
/**
* Creates a highly optimized signal with maximum performance and comprehensive error handling
* - Zero-allocation fast paths for identical values
* - Pre-bound methods to avoid .bind() overhead
* - Optimized function type checking
* - Cached observable pipeline
* - Comprehensive error handling with fallbacks
* - Memory leak prevention
*/
declare function createSignal<T>(initial: T): readonly [() => T, (value: T | ((prev: T) => T)) => void, Observable<T>];
/**
* Creates a highly optimized computed observable with advanced caching and error handling
* - WeakRef-based memory management
* - LRU cache for frequently used computations
* - Fast cache key generation
* - Automatic cleanup of stale references
* - Comprehensive error handling with fallbacks
*/
declare function createComputed<T, U>(source$: Observable<T>, compute: (value: T) => U): Observable<U>;
/**
* Advanced signal creation with performance monitoring (development only)
*/
declare function createSignalWithMetrics<T>(initial: T, name?: string): readonly [() => T, (value: T | ((prev: T) => T)) => void, Observable<T>];
/**
* Batch signal updates for maximum performance
* Groups multiple signal updates into a single microtask
*/
declare function batchSignalUpdates(updates: (() => void)[]): void;
/**
* Create multiple signals efficiently with shared configuration
*/
declare function createSignals<T extends Record<string, unknown>>(initialValues: T): {
[K in keyof T]: readonly [
() => T[K],
(value: T[K] | ((prev: T[K]) => T[K])) => void,
Observable<T[K]>
];
};
type Updater<T> = (prev: T) => T;
/**
* Creates a highly optimized store with maximum performance and comprehensive error handling
* - Ultra-fast shallow equality checking
* - Advanced LRU caching for selectors
* - Optimized object merging strategies
* - Pooled setter functions for reuse
* - Pre-compiled observable pipelines
* - Comprehensive error handling with fallbacks
* - Memory leak prevention
*/
declare function createStore<T extends object>(initial: T): readonly [() => T, (update: Partial<T> | Updater<T>) => void, Observable<T>, <K extends keyof T>(key: K) => Observable<T[K]>];
/**
* Advanced store creation with performance monitoring (development only)
*/
declare function createStoreWithMetrics<T extends object>(initial: T, name?: string): readonly [() => T, (update: Partial<T> | Updater<T>) => void, Observable<T>, <K extends keyof T>(key: K) => Observable<T[K]>];
/**
* Batch store updates for maximum performance
* Groups multiple store updates into a single microtask
*/
declare function batchStoreUpdates(updates: (() => void)[]): void;
/**
* Create multiple stores efficiently with shared configuration
*/
declare function createStores<T extends Record<string, object>>(initialValues: T): {
[K in keyof T]: readonly [
() => T[K],
(update: Partial<T[K]> | Updater<T[K]>) => void,
Observable<T[K]>,
<P extends keyof T[K]>(key: P) => Observable<T[K][P]>
];
};
/**
* Advanced selector with deep path support and caching
*/
declare function createDeepSelector<T extends object, R>(store: Observable<T>, path: string[], transform?: (value: unknown) => R): Observable<R>;
/**
* Optimized store state comparison utility
*/
declare function compareStoreStates<T extends object>(prev: T, next: T, keys?: (keyof T)[]): boolean;
/**
* Simplified and safe useSignal hook with RxJS optimizations
* - distinctUntilChanged to prevent repetitive values at source level
* - takeWhile for conditional subscription termination
* - take(1) for initial value fetching
* - Fast path optimizations with Object.is comparison
* - Safe error handling with fallbacks
*
* @example Basic usage:
* ```typescript
* const count = useSignal(counter$, 0);
* ```
*
* @example With condition (stops when count reaches 10):
* ```typescript
* const count = useSignal(counter$, 0, {
* condition: (value) => value < 10
* });
* ```
*
* @example Using utility conditions:
* ```typescript
* // Stop when value becomes falsy
* const value = useSignal(data$, '', {
* condition: SignalConditions.whileTruthy
* });
*
* // Stop when value goes above 100
* const score = useSignal(score$, 0, {
* condition: SignalConditions.whileBelow(100)
* });
*
* // Take only first 5 values
* const limited = useSignal(stream$, null, {
* condition: SignalConditions.takeCount(5)
* });
* ```
*
* @param source$ - The observable to subscribe to
* @param initial - Initial value to use
* @param options - Optional configuration
* @param options.condition - Function to control subscription continuation
*/
declare function useSignal<T>(source$: Observable<T>, initial: T, options?: {
condition?: (value: T) => boolean;
}): T;
/**
* Performance utility: No-op for API compatibility
*/
declare function clearSubscriptionPool(): void;
/**
* Performance utility: Get subscription statistics
*/
declare function getSubscriptionPoolStats(): {
cacheSize: string;
note: string;
};
/**
* Utility functions for common takeWhile conditions
*/
declare const SignalConditions: {
/**
* Take values while they are truthy
*/
readonly whileTruthy: <T>(value: T) => boolean;
/**
* Take values while they are below a threshold
*/
readonly whileBelow: (threshold: number) => (value: number) => boolean;
/**
* Take values while they are above a threshold
*/
readonly whileAbove: (threshold: number) => (value: number) => boolean;
/**
* Take values while they match a condition
*/
readonly whileMatches: <T>(predicate: (value: T) => boolean) => (value: T) => boolean;
/**
* Take values for a specific count
*/
readonly takeCount: (count: number) => <T>(_value: T) => boolean;
};
/**
* Enhanced useStore hook with RxJS optimizations
* - distinctUntilChanged to prevent repetitive values at source level
* - takeWhile for conditional subscription termination
* - take(1) for initial value fetching
* - Fast shallow equality for objects, Object.is for primitives
* - Safe error handling with fallbacks
*
* @example Basic usage:
* ```typescript
* const user = useStore(userStore$, { id: 0, name: '' });
* ```
*
* @example With condition (stops when user becomes inactive):
* ```typescript
* const user = useStore(userStore$, { id: 0, name: '', active: true }, {
* condition: (user) => user.active
* });
* ```
*
* @example Using utility conditions:
* ```typescript
* // Stop when store becomes falsy
* const data = useStore(dataStore$, null, {
* condition: StoreConditions.whileTruthy
* });
*
* // Stop when specific property changes
* const state = useStore(appState$, initialState, {
* condition: StoreConditions.whilePropertyEquals('status', 'loading')
* });
* ```
*
* @param source$ - The observable store to subscribe to
* @param initial - Initial value to use
* @param options - Optional configuration
* @param options.condition - Function to control subscription continuation
*/
declare function useStore<T>(source$: Observable<T>, initial: T, options?: {
condition?: (value: T) => boolean;
}): T;
/**
* Utility functions for common store takeWhile conditions
*/
declare const StoreConditions: {
/**
* Take values while they are truthy
*/
readonly whileTruthy: <T>(value: T) => boolean;
/**
* Take values while a specific property equals a value
*/
readonly whilePropertyEquals: <T extends Record<string, any>, K extends keyof T>(property: K, expectedValue: T[K]) => (value: T) => boolean;
/**
* Take values while a specific property is truthy
*/
readonly whilePropertyTruthy: <T extends Record<string, any>, K extends keyof T>(property: K) => (value: T) => boolean;
/**
* Take values while all specified properties are truthy
*/
readonly whilePropertiesTruthy: <T extends Record<string, any>>(...properties: (keyof T)[]) => (value: T) => boolean;
/**
* Take values while store matches a condition
*/
readonly whileMatches: <T>(predicate: (value: T) => boolean) => (value: T) => boolean;
/**
* Take values for a specific count
*/
readonly takeCount: (count: number) => <T>(_value: T) => boolean;
/**
* Take values while store has a specific structure
*/
readonly whileHasProperty: <T extends Record<string, any>>(property: keyof T) => (value: T) => boolean;
};
/**
* Track lifecycle of a signal or store field
* @param source$ - RxJS Observable (signal or store selector)
* @param get - function to get the current value
* @param callback - function called with (prev, current) on each update
*/
declare function useSignalLifecycle<T>(source$: Observable<T>, get: () => T, callback: (prev: T | undefined, current: T) => void): void;
/**
* Hook that runs an effect when signal values change
* Alternative to useEffect that works seamlessly with signals
*
* @param source$ - RxJS Observable to subscribe to
* @param effect - Effect function that runs when signal changes
* @param deps - Optional dependency list for effect updates
*/
declare function useSignalEffect<T>(source$: Observable<T>, effect: (value: T) => void | (() => void), deps?: DependencyList): void;
/**
* Hook that provides the current value of an observable for use in useEffect dependencies
* Solves the issue where signals don't trigger useEffect when used directly
*
* @param source$ - RxJS Observable to subscribe to
* @param initial - Initial value before first emission
* @returns Current value of the observable
*/
declare function useSignalValue<T>(source$: Observable<T>, initial: T): T;
/**
* Hook that provides a callback to manually trigger effects based on signal state
* Useful for imperative operations that need to respond to signal changes
*
* @param source$ - RxJS Observable to track
* @param callback - Function to call with current signal value
* @returns Function that when called, executes callback with current signal value
*/
declare function useSignalCallback<T, R>(source$: Observable<T>, callback: (value: T) => R): () => R;
/**
* Hook that debounces signal changes for use in effects
* Prevents excessive effect executions when signals change rapidly
*
* @param source$ - RxJS Observable to subscribe to
* @param effect - Effect function that runs after debounce delay
* @param delay - Debounce delay in milliseconds
* @param deps - Optional dependency list for effect updates
*/
declare function useDebouncedSignalEffect<T>(source$: Observable<T>, effect: (value: T) => void | (() => void), delay: number, deps?: DependencyList): void;
/**
* Hook that automatically memoizes all children to prevent unnecessary re-renders
* Perfect for components that use signals but want to prevent child re-renders
*
* @example
* ```tsx
* function Counter() {
* const count = useSignal(count$, 0);
* const memoizeChildren = useAutoMemo();
*
* return (
* <div>
* <p>Count: {count}</p>
* <button onClick={() => setCount(c => c + 1)}>Increment</button>
* {memoizeChildren(<Child />)}
* {memoizeChildren(<AnotherChild data="static" />)}
* </div>
* );
* }
* ```
*/
declare function useAutoMemo(): (element: React.ReactElement) => React.ReactElement;
/**
* Decorator to automatically optimize child components
* Use this around components that use signals to prevent child re-renders
*
* @example
* ```tsx
* const OptimizedCounter = withAutoMemo(function Counter() {
* const count = useSignal(count$, 0);
*
* return (
* <div>
* <p>Count: {count}</p>
* <Child />
* <AnotherChild />
* </div>
* );
* });
* ```
*/
declare function withAutoMemo<T extends Record<string, unknown>>(Component: React.ComponentType<T>): React.ComponentType<T>;
/**
* Hook to create a static component that never re-renders
* Perfect for components with no dependencies
*
* @example
* ```tsx
* function Parent() {
* const count = useSignal(count$, 0);
* const StaticChild = useStaticMemo(() =>
* React.createElement('div', {}, 'I never re-render!')
* );
*
* return React.createElement('div', {},
* React.createElement('p', {}, 'Count: ', count),
* React.createElement(StaticChild)
* );
* }
* ```
*/
declare function useStaticMemo<T extends Record<string, unknown>>(renderFn: () => React.ReactElement<T>): React.ComponentType<T>;
/**
* useSignalTextProps
* Return DOM attributes to bind an element's textContent directly to an Observable
* No refs, no effects in the hook; global MutationObserver manages lifecycle
*/
declare function useSignalTextProps<T>(source$: Observable<T>, initialValue: T, transform?: (value: T) => string): Record<string, string>;
/**
* TRUE fine-grained hook that updates DOM directly without component re-renders
* Returns a ref callback to attach to DOM elements for direct updates
* ✅ Zero re-renders - children never re-render
*
* @example
* // ✅ Zero re-renders - children never re-render
* const countRef = useTrueFineGrainedValue(count$, 0);
* // Use ref={countRef} on a span/div to display the reactive value
*/
declare function useTrueFineGrainedValue<T>(source$: Observable<T>, initialValue: T, transform?: (value: T) => string): (element: HTMLElement | null) => void;
/**
* Component that renders reactive text with direct DOM updates
* No component re-renders, just DOM text updates
*
* @example
* // Zero re-renders with direct DOM text updates
* React.createElement(FineGrainedText, { source: count$ })
*/
declare function FineGrainedText<T>({ source, transform, className, style, ...props }: {
source: Observable<T>;
transform?: (value: T) => string;
className?: string;
style?: React.CSSProperties;
[key: string]: unknown;
}): React.DetailedReactHTMLElement<{
ref: React.RefObject<HTMLSpanElement>;
className: string | undefined;
style: React.CSSProperties | undefined;
}, HTMLSpanElement>;
/**
* Creates a fine-grained reactive attribute that updates independently
* Updates DOM attributes directly without component re-renders
*
* @example
* // Direct DOM attribute updates without re-renders
* const attrs = useFineGrainedAttr(progress$, 'data-progress');
* // Use {...attrs} to spread onto element
*/
declare function useFineGrainedAttr<T>(source$: Observable<T>, attributeName: string, transform?: (value: T) => string): Record<string, unknown>;
/**
* Creates a reactive class name that updates independently
*
* @example
* // Direct CSS class updates without re-renders
* const classAttrs = useFineGrainedClass(isActive$, active => active ? 'active' : '');
*/
declare function useFineGrainedClass<T>(source$: Observable<T>, transform: (value: T) => string): Record<string, unknown>;
/**
* Creates a reactive style that updates independently
*
* @example
* // Direct CSS style updates without re-renders
* const styleAttrs = useFineGrainedStyle(color$, color => `background-color: ${color}`);
*/
declare function useFineGrainedStyle<T>(source$: Observable<T>, transform: (value: T) => string): Record<string, unknown>;
/**
* SolidJS-like Show component with fine-grained reactivity
* Only updates visibility without re-rendering children
*
* @example
* // Conditional rendering without parent re-renders
* React.createElement(FineGrainedShow, { when: showChild$ }, children);
*/
declare function FineGrainedShow<T>({ when, fallback, children, }: {
when: Observable<T>;
fallback?: React.ReactNode;
children: React.ReactNode;
}): React.FunctionComponentElement<{}> | null;
/**
* SolidJS-like For component with fine-grained reactivity
* Only updates list items that actually changed
*
* @example
* // List rendering with minimal updates
* React.createElement(FineGrainedFor, { each: todos$ }, renderItemFunction);
*/
declare function FineGrainedFor<T>({ each, children, }: {
each: Observable<T[]>;
children: (item: T, index: number) => React.ReactNode;
}): React.FunctionComponentElement<{}>;
/**
* Wraps a component to use fine-grained reactivity automatically
* Makes any component behave like SolidJS components
*
* @example
* // Wraps a component to prevent re-renders
* const OptimizedCounter = withFineGrainedReactivity(MyComponent);
*/
declare function withFineGrainedReactivity<T extends Record<string, unknown>>(Component: React.ComponentType<T>): React.ComponentType<T>;
/**
* Performance utility: Clean up all reactive subscriptions
*/
declare function cleanupFineGrainedReactivity(): void;
/**
* Performance utility: Get fine-grained reactivity statistics
*/
declare function getFineGrainedStats(): {
note: string;
memoryManagement: string;
performance: string;
};
/**
* Creates a highly optimized memoized component for signals
* - Ultra-fast observable detection with caching
* - Optimized prop comparison with early termination
* - Component instance caching to avoid recreation
* - Pre-compiled constants for maximum speed
*/
declare function createSignalMemo<T extends Record<string, unknown>>(Component: React.ComponentType<T>): React.MemoExoticComponent<React.ComponentType<T>>;
/**
* Optimized shallow comparison memo with component caching
*/
declare function createShallowMemo<T extends Record<string, unknown>>(Component: React.ComponentType<T>): React.MemoExoticComponent<React.ComponentType<T>>;
/**
* Ultra-optimized signal tracking component with advanced caching
* - Component instance caching based on signal signatures
* - Fast signal array comparison
* - Minimal re-creation overhead
* - Optimized ref management
*/
declare function withSignalTracking<T extends Record<string, unknown>>(Component: React.ComponentType<T>, signals: Observable<unknown>[]): React.ComponentType<T>;
/**
* Ultra-optimized component that prevents unnecessary re-renders
* - Advanced deep equality with caching
* - Component instance caching
* - Development-only logging with minimal overhead
* - Fast path optimizations
*/
declare function preventUnnecessaryRerenders<T extends Record<string, unknown>>(Component: React.ComponentType<T>): React.MemoExoticComponent<React.ComponentType<T>>;
/**
* Auto-memoizes any component to prevent unnecessary re-renders
* Perfect for child components that don't use signals
*
* @example
* ```tsx
* const OptimizedChild = autoMemo(() => {
* console.log('This will only render once!');
* return <div>Static content</div>;
* });
* ```
*/
declare function autoMemo<T extends Record<string, unknown>>(Component: React.ComponentType<T>): React.MemoExoticComponent<React.ComponentType<T>>;
/**
* Creates a static component that NEVER re-renders after mount
* Use for components with no props that display static content
*
* @example
* ```tsx
* const StaticChild = staticMemo(() => {
* console.log('Renders only once ever!');
* return <div>I never re-render</div>;
* });
* ```
*/
declare function staticMemo<T extends Record<string, unknown>>(Component: React.ComponentType<T>): React.MemoExoticComponent<React.ComponentType<T>>;
/**
* Optimizes a component to only re-render when specific props change
*
* @param Component - The component to memoize
* @param propKeys - Array of prop keys that should trigger re-renders
*
* @example
* ```tsx
* const OptimizedComponent = propsMemo(MyComponent, ['data', 'isLoading']);
* // Only re-renders when data or isLoading props change
* ```
*/
declare function propsMemo<T extends Record<string, unknown>>(Component: React.ComponentType<T>, propKeys: (keyof T)[]): React.MemoExoticComponent<React.ComponentType<T>>;
/**
* Hook to automatically memoize child components at runtime
* Use this in parent components to prevent child re-renders
*
* @example
* ```tsx
* function Parent() {
* const count = useSignal(count$, 0);
* const MemoizedChild = useMemoizedComponent(Child);
*
* return (
* <div>
* <p>Count: {count}</p>
* <MemoizedChild />
* </div>
* );
* }
* ```
*/
declare function useMemoizedComponent<T extends Record<string, unknown>>(Component: React.ComponentType<T>): React.MemoExoticComponent<React.ComponentType<T>>;
/**
* Performance utility: Clear all internal caches
* Useful for memory management in long-running applications
* Note: WeakMaps don't have clear() method, they auto-cleanup
*/
declare function clearMemoizationCaches(): void;
/**
* Performance utility: Get cache statistics
* Note: WeakMaps don't expose size information
*/
declare function getMemoizationStats(): {
componentMemoCache: string;
signalTrackingCache: string;
note: string;
};
/**
* Creates a fine-grained selector that only updates when the selected value changes
* This helps reduce re-renders by only subscribing to specific parts of state
*/
declare function createSelector<T, R>(source$: Observable<T>, selector: (value: T) => R, equalityFn?: (a: R, b: R) => boolean): Observable<R>;
/**
* Creates multiple selectors at once for better performance
*/
declare function createSelectors<T, R extends Record<string, unknown>>(source$: Observable<T>, selectors: {
[K in keyof R]: (value: T) => R[K];
}, equalityFn?: (a: R[keyof R], b: R[keyof R]) => boolean): {
[K in keyof R]: Observable<R[K]>;
};
/**
* Shallow equality function for objects
*/
declare function shallowEqual<T>(a: T, b: T): boolean;
/**
* Creates a derived value that depends on multiple signals
* Only updates when any of the dependencies change
*/
declare function createDerived<T, R>(source$: Observable<T>, deriveFn: (value: T) => R, equalityFn?: (a: R, b: R) => boolean): Observable<R>;
/**
* Creates a selector with memoization for expensive computations
*/
declare function createMemoizedSelector<T, R>(source$: Observable<T>, selector: (value: T) => R, equalityFn?: (a: R, b: R) => boolean): Observable<R>;
export { FineGrainedFor, FineGrainedShow, FineGrainedText, SignalConditions, StoreConditions, autoMemo, batchSignalUpdates, batchStoreUpdates, cleanupFineGrainedReactivity, clearMemoizationCaches, clearSubscriptionPool, compareStoreStates, createComputed, createDeepSelector, createDerived, createMemoizedSelector, createSelector, createSelectors, createShallowMemo, createSignal, createSignalMemo, createSignalWithMetrics, createSignals, createStore, createStoreWithMetrics, createStores, getFineGrainedStats, getMemoizationStats, getSubscriptionPoolStats, preventUnnecessaryRerenders, propsMemo, shallowEqual, staticMemo, useAutoMemo, useDebouncedSignalEffect, useFineGrainedAttr, useFineGrainedClass, useFineGrainedStyle, useMemoizedComponent, useSignal, useSignalCallback, useSignalEffect, useSignalLifecycle, useSignalTextProps, useSignalValue, useStaticMemo, useStore, useTrueFineGrainedValue, withAutoMemo, withFineGrainedReactivity, withSignalTracking };