createSharedComposable
Creates a shared instance of a wrapped composable function that uses reference counting. When the last consumer is destroyed, the shared instance and its resources are cleaned up automatically.
Usage
Basic Usage Without Parameters
typescript
import { createSharedComposable } from 'ng-reactive-utils';
import { signal } from '@angular/core';
const useWebSocket = createSharedComposable(() => {
const socket = new WebSocket('wss://api.example.com');
const messages = signal<string[]>([]);
socket.onmessage = (event) => {
messages.update((m) => [...m, event.data]);
};
return {
value: messages.asReadonly(),
cleanup: () => socket.close(),
};
});
@Component({
template: `<div>{{ messages() | json }}</div>`,
})
class ChatComponent {
messages = useWebSocket();
}With Parameters
typescript
import { createSharedComposable } from 'ng-reactive-utils';
import { inject, signal } from '@angular/core';
import { DOCUMENT } from '@angular/common';
const useMediaQuery = createSharedComposable((query: string) => {
const document = inject(DOCUMENT);
const mediaQuery = document.defaultView?.matchMedia(query);
const matches = signal(mediaQuery?.matches ?? false);
const handleChange = (event: MediaQueryListEvent) => matches.set(event.matches);
mediaQuery?.addEventListener('change', handleChange);
return {
value: matches.asReadonly(),
cleanup: () => mediaQuery?.removeEventListener('change', handleChange),
};
});
@Component({
template: `<nav>{{ isMobile() ? 'Mobile' : 'Desktop' }} layout</nav>`,
})
class NavComponent {
isMobile = useMediaQuery('(max-width: 768px)');
}Parameters
| Parameter | Type | Description |
|---|---|---|
factory | (...args: Args) => ComposableResult<T> | A factory function that creates the composable instance. Must return an object with value and optional cleanup function. |
Returns
A function (...args: Args) => T that returns the shared instance value. The function can be called with the same arguments as the factory function.
ComposableResult Interface
The factory function must return an object with:
value: T- The value to be shared across all consumerscleanup?: () => void- Optional cleanup function called when the last consumer is destroyed
Notes
- Reference Counting: Multiple components using the same composable with the same arguments share a single instance
- Automatic Cleanup: When the last consumer component is destroyed, the cleanup function is called automatically
- Argument-Based Caching: Different argument values create separate cached instances
- DestroyRef Integration: Automatically registers cleanup handlers using Angular's
DestroyRef - Memory Efficient: Prevents resource leaks by ensuring cleanup runs when no more consumers exist
- Use this utility when you want to share expensive resources (like WebSocket connections, event listeners, or API subscriptions) across multiple components
Source
ts
import { DestroyRef, inject } from '@angular/core';
interface ComposableResult<T> {
value: T;
cleanup?: () => void;
}
interface CacheEntry<T> {
result: T;
refCount: number;
cleanup?: () => void;
}
/**
* Creates a shared instance of a wrapped composable function that uses reference counting.
* When the last consumer is destroyed, the shared instance and its resources are cleaned up automatically.
*
* @example
* // Basic usage without parameters
* const useWebSocket = createSharedComposable(() => {
* const socket = new WebSocket('wss://api.example.com');
* const messages = signal<string[]>([]);
*
* socket.onmessage = (event) => {
* messages.update((m) => [...m, event.data]);
* };
*
* return {
* value: messages.asReadonly(),
* cleanup: () => socket.close(),
* };
* });
*
* @example
* // With parameters
* const useMediaQuery = createSharedComposable((query: string) => {
* const document = inject(DOCUMENT);
* const mediaQuery = document.defaultView?.matchMedia(query);
* const matches = signal(mediaQuery?.matches ?? false);
*
* const handleChange = (event: MediaQueryListEvent) => matches.set(event.matches);
* mediaQuery?.addEventListener('change', handleChange);
*
* return {
* value: matches.asReadonly(),
* cleanup: () => mediaQuery?.removeEventListener('change', handleChange),
* };
* });
*/
export function createSharedComposable<T, Args extends any[]>(
factory: (...args: Args) => ComposableResult<T>,
): (...args: Args) => T {
// Cache is scoped to the factory function itself
const cache = new Map<string, CacheEntry<T>>();
return (...args: Args): T => {
const destroyRef = inject(DestroyRef);
// Create cache key from arguments
const cacheKey = args.length > 0 ? JSON.stringify(args) : '__default__';
// Get or create cached entry
let entry = cache.get(cacheKey);
if (!entry) {
// Create new instance
const result = factory(...args);
entry = {
result: result.value,
refCount: 0,
cleanup: result.cleanup,
};
cache.set(cacheKey, entry);
}
// Increment reference count
entry.refCount++;
// Register cleanup on component destruction
destroyRef.onDestroy(() => {
if (entry) {
entry.refCount--;
// If no more references, cleanup and remove from cache
if (entry.refCount <= 0) {
entry.cleanup?.();
cache.delete(cacheKey);
}
}
});
return entry.result;
};
}