Documentation Index
Fetch the complete documentation index at: https://mintlify.com/akbarsaputrait/ngememoize/llms.txt
Use this file to discover all available pages before exploring further.
Ngememoize automatically detects and handles asynchronous functions that return Promises. This guide covers how to memoize async operations effectively.
How Ngememoize handles Promises
When your memoized function returns a Promise, Ngememoize:
- Detects the Promise using
isPromise(result)
- Waits for the Promise to resolve
- Caches the Promise itself (not just the resolved value)
- Returns the cached Promise for subsequent calls with the same arguments
Ngememoize caches the Promise object, which means all callers share the same Promise instance for identical arguments.
Basic async memoization
Apply @Ngememoize to async methods just like synchronous ones:
import { Component } from '@angular/core';
import { Ngememoize } from 'ngememoize';
@Component({
selector: 'app-data',
standalone: true,
})
export class DataComponent {
@Ngememoize({
debugLabel: 'fetchData',
maxAge: 5000
})
async fetchData(id: string): Promise<string> {
console.log('Fetching...');
return new Promise(resolve =>
setTimeout(() => resolve(`Data for ${id}`), 1000)
);
}
}
Usage:
// First call - executes the async function
await this.fetchData('123'); // Logs: "Fetching..."
// Waits 1 second, returns: "Data for 123"
// Second call with same ID - returns cached Promise
await this.fetchData('123'); // No log, instant return
// Returns: "Data for 123" (from cache)
// Different ID - executes again
await this.fetchData('456'); // Logs: "Fetching..."
// Waits 1 second, returns: "Data for 456"
Promise caching behavior
From the Ngememoize source code:
const result = fn.apply(context, args);
if (isPromise(result)) {
return result.then(resolvedResult => {
if (
(Array.isArray(resolvedResult) && resolvedResult.length > 0) ||
resolvedResult
) {
cache.set(key, { value: result, timestamp: now, generatedKey });
}
return resolvedResult;
}) as TResult;
}
Key points:
- The cache stores the Promise (
result), not the resolved value
- Empty arrays and falsy values are not cached
- The Promise is cached after it resolves successfully
- The timestamp is set when the function is first called, not when it resolves
Async/await patterns
HTTP requests
Memoize API calls to prevent duplicate requests:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Ngememoize } from 'ngememoize';
import { firstValueFrom } from 'rxjs';
interface User {
id: number;
firstName: string;
lastName: string;
}
@Injectable({
providedIn: 'root',
})
export class UserService {
private apiUrl = 'https://api.example.com/users';
constructor(private http: HttpClient) {}
@Ngememoize({
maxAge: 60000, // Cache for 1 minute
debugLabel: 'fetchUser'
})
async fetchUser(id: number): Promise<User> {
const response = await firstValueFrom(
this.http.get<User>(`${this.apiUrl}/${id}`)
);
return response;
}
}
Multiple parallel calls
When multiple components request the same data simultaneously, they all receive the same cached Promise:
// Component A
const userA = await this.userService.fetchUser(1); // Makes HTTP request
// Component B (called while Component A's request is pending)
const userB = await this.userService.fetchUser(1); // Gets same Promise, no new request
Both components share the same Promise and receive the result when it resolves.
Time-based expiration
Use maxAge to refresh cached async results periodically:
@Ngememoize({
maxAge: 30000, // Refresh every 30 seconds
debugLabel: 'fetchNews'
})
async fetchNews(category: string): Promise<Article[]> {
const response = await fetch(`/api/news/${category}`);
return response.json();
}
After 30 seconds:
- The cache entry is deleted
- The next call triggers a new request
- The new Promise is cached
Error handling
Memoized async functions handle errors the same way as unmemoized ones:
@Ngememoize({
debugLabel: 'fetchUser'
})
async fetchUser(id: number): Promise<User> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
} catch (error) {
console.error('Failed to fetch user:', error);
throw error;
}
}
Calling code:
try {
const user = await this.fetchUser(123);
console.log(user);
} catch (error) {
// Handle error
console.error('Error:', error);
}
If a Promise rejects, it is still cached. Subsequent calls with the same arguments will return the same rejected Promise until the cache expires or is cleared.
Observables vs Promises
Ngememoize works with Promises, not RxJS Observables. Convert Observables to Promises:
import { firstValueFrom } from 'rxjs';
@Ngememoize({
maxAge: 5000
})
async searchUsers(query: string): Promise<User[]> {
const response = await firstValueFrom(
this.http.get<{ users: User[] }>(`/api/users/search?q=${query}`)
);
return response.users;
}
Use firstValueFrom to convert an Observable to a Promise that resolves with the first emitted value.
Combine debugLabel and callbacks to track async operations:
@Ngememoize({
maxAge: 5000,
debugLabel: 'fetchData',
onCacheHit: (key) => {
console.log(`✓ Returning cached data for ${key}`);
},
onCacheMiss: (key) => {
console.log(`⟳ Fetching fresh data for ${key}`);
}
})
async fetchData(id: string): Promise<Data> {
console.log(`Starting fetch for ID: ${id}`);
const response = await fetch(`/api/data/${id}`);
return response.json();
}
Console output for cached call:
[Memoize: fetchData] Cache hit for key: user-123
✓ Returning cached data for user-123
Console output for cache miss:
[Memoize: fetchData] Cache miss for key: user-456
⟳ Fetching fresh data for user-456
Starting fetch for ID: user-456
Real-world example
Here’s a complete example from the Ngememoize README:
import { Component } from '@angular/core';
import { Ngememoize } from 'ngememoize';
@Component({
selector: 'app-data',
standalone: true,
})
export class DataComponent {
@Ngememoize({
debugLabel: 'fetchData',
maxAge: 5000
})
async fetchData(id: string): Promise<string> {
console.log('Fetching...');
return new Promise(resolve =>
setTimeout(() => resolve(`Data for ${id}`), 1000)
);
}
async loadData() {
// First call
const data1 = await this.fetchData('user-1'); // Logs: "Fetching..."
console.log(data1); // "Data for user-1" after 1 second
// Second call (cached)
const data2 = await this.fetchData('user-1'); // No log, instant
console.log(data2); // "Data for user-1" immediately
// After 5 seconds, cache expires
setTimeout(async () => {
const data3 = await this.fetchData('user-1'); // Logs: "Fetching..."
console.log(data3); // "Data for user-1" after 1 second
}, 6000);
}
}
Best practices
Cache API responses
Prevents redundant network requests:
@Ngememoize({ maxAge: 60000 })
async getProducts(): Promise<Product[]> {
return firstValueFrom(this.http.get<Product[]>('/api/products'));
}
Use appropriate maxAge
Balance freshness with performance:
- Frequently changing data: 10-30 seconds
- Moderately stable data: 1-5 minutes
- Rarely changing data: 10-60 minutes
Don’t cache user-specific actions
Avoid memoizing operations with side effects:
// DON'T memoize this
async submitOrder(order: Order): Promise<void> {
await this.http.post('/api/orders', order);
}
// DO memoize this
@Ngememoize({ maxAge: 30000 })
async getOrderHistory(userId: string): Promise<Order[]> {
return firstValueFrom(this.http.get<Order[]>(`/api/orders/${userId}`));
}
Next steps