ng-memoize-data-storage
v1.0.1
Published
A TypeScript utility library for memoizing data storage in Angular applications with HTTP caching
Downloads
13
Maintainers
Readme
ng-memoize-data-storage
A lightweight TypeScript utility library for memoizing data storage in Angular applications with HTTP caching capabilities using Angular signals.
✨ Features
- 🔄 Smart Memoization: Automatic HTTP request caching to avoid redundant API calls
- 📡 Angular HttpClient Integration: Built-in support for Angular's HttpClient
- 📊 Reactive Signals: Uses Angular signals for reactive state management
- 🚀 Loading States: Built-in loading state management with automatic indicators
- 🎯 Full TypeScript Support: Complete type safety and IntelliSense
- 📦 Tree-shakeable: Import only what you need for optimal bundle size
- 🎨 Generic Types: Works with any data type - from simple primitives to complex objects
- 🔧 Utility Types: Includes common data structure interfaces
- ⚡ Zero Runtime Dependencies: Lightweight with only peer dependencies
- 🛡️ Error Handling: Robust error handling with automatic state recovery
📦 Installation
npm install ng-memoize-data-storagePeer Dependencies
Make sure you have the required Angular dependencies:
npm install @angular/common @angular/core rxjs🚀 Quick Start
Basic Usage
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { MemoizedDataStorage } from 'ng-memoize-data-storage';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user',
template: `
<div>
<button (click)="loadUser()" [disabled]="userStorage.isLoading()">
{{ userStorage.isLoading() ? 'Loading...' : 'Load User' }}
</button>
@if (userStorage.singleData(); as user) {
<div class="user-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
}
</div>
`
})
export class UserComponent {
private httpClient = inject(HttpClient);
// Create a memoized storage for User data
userStorage = new MemoizedDataStorage<User>(this.httpClient);
async loadUser() {
// This will only make an HTTP request on the first call
// Subsequent calls return cached data instantly
await this.userStorage.loadSingleData('/api/user/1');
}
}Multiple Data Loading
@Component({
selector: 'app-users-list',
template: `
<div>
<button (click)="loadUsers()">
{{ usersStorage.isLoading() ? 'Loading...' : 'Load Users' }}
</button>
<div class="users-grid">
@for (user of usersStorage.multipleData(); track user.id) {
<div class="user-card">
{{ user.name }} - {{ user.email }}
</div>
}
</div>
</div>
`
})
export class UsersListComponent {
private httpClient = inject(HttpClient);
usersStorage = new MemoizedDataStorage<User>(this.httpClient);
async loadUsers() {
await this.usersStorage.loadMultipleData('/api/users', {
page: 1,
limit: 10,
status: 'active'
});
}
}📚 Advanced Usage
Working with API Response Wrappers
import { ApiResponse } from 'ng-memoize-data-storage';
@Component({
selector: 'app-api-wrapper-example',
template: `
@if (userApiStorage.singleData(); as response) {
@if (response.success) {
<div>✅ {{ response.data.name }}</div>
} @else {
<div>❌ Error: {{ response.message }}</div>
}
}
`
})
export class ApiWrapperComponent {
private httpClient = inject(HttpClient);
userApiStorage = new MemoizedDataStorage<ApiResponse<User>>(this.httpClient);
async loadUser() {
await this.userApiStorage.loadSingleData('/api/user/1');
}
}Using KeyData Utility Type
import { KeyData } from 'ng-memoize-data-storage';
@Component({
selector: 'app-key-value-example',
template: `
@for (item of dataStorage.multipleData(); track item.key) {
<div>{{ item.key }}: {{ item.value }}</div>
}
`
})
export class KeyValueComponent {
private httpClient = inject(HttpClient);
dataStorage = new MemoizedDataStorage<KeyData<number, string>>(this.httpClient);
async loadData() {
await this.dataStorage.loadMultipleData('/api/key-value-pairs');
}
}Cache Management
export class CacheManagementComponent {
private httpClient = inject(HttpClient);
userStorage = new MemoizedDataStorage<User>(this.httpClient);
async refreshData() {
// Force fresh data fetch, bypassing cache
this.userStorage.disableMemoizationOnNextRead();
await this.userStorage.loadSingleData('/api/user/1');
}
clearCache() {
// Clear all cached data
this.userStorage.clear();
}
checkCacheStatus() {
if (this.userStorage.hasSingleData()) {
console.log('✅ Data is cached');
} else {
console.log('❌ No cached data');
}
}
}🎯 Real-World Examples
E-commerce Product Catalog
interface Product {
id: number;
name: string;
price: number;
category: string;
}
@Component({
selector: 'app-product-catalog',
template: `
<div class="catalog">
<div class="filters">
<select (change)="filterByCategory($event)">
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<button (click)="refreshProducts()">🔄 Refresh</button>
</div>
@if (productsStorage.isLoading()) {
<div class="loading">Loading products...</div>
}
<div class="products-grid">
@for (product of productsStorage.multipleData(); track product.id) {
<div class="product-card">
<h3>{{ product.name }}</h3>
<p class="price">${{ product.price }}</p>
<span class="category">{{ product.category }}</span>
</div>
}
</div>
</div>
`
})
export class ProductCatalogComponent {
private httpClient = inject(HttpClient);
productsStorage = new MemoizedDataStorage<Product>(this.httpClient);
async ngOnInit() {
await this.loadProducts();
}
async loadProducts(category?: string) {
const params = category ? { category } : {};
await this.productsStorage.loadMultipleData('/api/products', params);
}
async filterByCategory(event: Event) {
const category = (event.target as HTMLSelectElement).value;
// Clear cache when filtering to ensure fresh results
this.productsStorage.disableMemoizationOnNextRead();
await this.loadProducts(category || undefined);
}
async refreshProducts() {
this.productsStorage.disableMemoizationOnNextRead();
await this.loadProducts();
}
}User Profile Dashboard
interface UserProfile {
id: number;
name: string;
email: string;
avatar: string;
preferences: {
theme: string;
notifications: boolean;
};
}
@Component({
selector: 'app-profile-dashboard',
template: `
<div class="dashboard">
@if (profileStorage.singleData(); as profile) {
<div class="profile-header">
<img [src]="profile.avatar" [alt]="profile.name">
<div>
<h2>{{ profile.name }}</h2>
<p>{{ profile.email }}</p>
</div>
<button (click)="refreshProfile()" [disabled]="profileStorage.isLoading()">
{{ profileStorage.isLoading() ? '🔄' : '🔄 Refresh' }}
</button>
</div>
<div class="preferences">
<h3>Preferences</h3>
<p>Theme: {{ profile.preferences.theme }}</p>
<p>Notifications: {{ profile.preferences.notifications ? 'On' : 'Off' }}</p>
</div>
} @else {
<div class="loading">Loading profile...</div>
}
</div>
`
})
export class ProfileDashboardComponent {
private httpClient = inject(HttpClient);
profileStorage = new MemoizedDataStorage<UserProfile>(this.httpClient);
async ngOnInit() {
await this.loadProfile();
}
async loadProfile() {
try {
await this.profileStorage.loadSingleData('/api/user/profile');
} catch (error) {
console.error('Failed to load profile:', error);
// Handle error (show toast, redirect to login, etc.)
}
}
async refreshProfile() {
this.profileStorage.disableMemoizationOnNextRead();
await this.loadProfile();
}
}🔧 API Reference
MemoizedDataStorage<T>
Constructor
constructor(httpClient: HttpClient)Properties
singleData: Signal<T | null>- Read-only signal containing single data objectmultipleData: Signal<T[]>- Read-only signal containing array of data objectsisLoading: Signal<boolean>- Read-only signal indicating loading state
Methods
loadSingleData(url: string, queryParams?: Record<string, string | number>): Promise<void>
Loads a single data object from the specified URL. Uses memoization to avoid redundant requests.
loadMultipleData(url: string, queryParams?: Record<string, string | number>): Promise<void>
Loads multiple data objects from the specified URL. Uses memoization to avoid redundant requests.
disableMemoizationOnNextRead(): void
Forces the next load operation to fetch fresh data, bypassing the cache.
clear(): void
Clears all cached data and resets loading state.
hasSingleData(): boolean
Returns true if single data is currently cached.
hasMultipleData(): boolean
Returns true if multiple data is currently cached (non-empty array).
Utility Types
KeyData<K, V>
interface KeyData<K, V> {
key: K;
value: V;
}ApiResponse<T>
interface ApiResponse<T> {
data: T;
message?: string;
status: number;
success: boolean;
}PaginatedResponse<T>
interface PaginatedResponse<T> {
data: T[];
meta: PaginationMeta;
}Utility Functions
createKeyData<K, V>(key: K, value: V): KeyData<K, V>
Creates a key-value pair object.
isApiResponse<T>(response: any): response is ApiResponse<T>
Type guard to check if a response follows the ApiResponse interface.
isPaginatedResponse<T>(response: any): response is PaginatedResponse<T>
Type guard to check if a response follows the PaginatedResponse interface.
HTTP Context Tokens
SkipLoadingSpinner
HTTP context token that can be used in interceptors to skip loading spinner display:
import { SkipLoadingSpinner } from 'ng-memoize-data-storage';
// The library automatically sets this token on its requests
// Your interceptor can check for this token to conditionally show/hide loading spinners🛠️ Error Handling
The library provides robust error handling:
async loadData() {
try {
await this.dataStorage.loadSingleData('/api/data');
} catch (error) {
if (error instanceof HttpErrorResponse) {
switch (error.status) {
case 404:
console.error('Data not found');
break;
case 403:
console.error('Access denied');
break;
default:
console.error('An error occurred:', error.message);
}
}
}
}🏗️ Integration with Angular
Module Setup
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [
HttpClientModule, // Required for HttpClient
// ... other imports
],
// ...
})
export class AppModule { }Standalone Components
import { Component } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
@Component({
selector: 'app-standalone',
standalone: true,
imports: [HttpClientModule],
// ...
})
export class StandaloneComponent { }🚀 Performance Tips
Reuse Storage Instances: Create storage instances as component properties to maintain cache across component lifecycle.
Strategic Cache Clearing: Only clear cache when necessary (e.g., after data mutations).
Query Parameter Caching: Different query parameters create separate cache entries.
Loading State Optimization: Use the
isLoadingsignal to provide immediate UI feedback.
🧪 Testing
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { MemoizedDataStorage } from 'ng-memoize-data-storage';
describe('MemoizedDataStorage', () => {
let httpTestingController: HttpTestingController;
let storage: MemoizedDataStorage<any>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule]
});
httpTestingController = TestBed.inject(HttpTestingController);
const httpClient = TestBed.inject(HttpClient);
storage = new MemoizedDataStorage(httpClient);
});
it('should cache data after first request', async () => {
const testData = { id: 1, name: 'Test' };
// First request
const promise1 = storage.loadSingleData('/api/test');
const req1 = httpTestingController.expectOne('/api/test');
req1.flush(testData);
await promise1;
expect(storage.singleData()).toEqual(testData);
// Second request should use cache (no HTTP call)
await storage.loadSingleData('/api/test');
httpTestingController.expectNone('/api/test');
expect(storage.singleData()).toEqual(testData);
});
});📝 Changelog
See GitHub Releases for detailed changelog.
🤝 Contributing
Contributions are welcome! Please read our Contributing Guidelines before submitting PRs.
Development Setup
# Clone the repository
git clone https://github.com/saiful-70/ng-memoize-data-storage.git
# Install dependencies
npm install
# Run type checking
npm run type-check
# Build the package
npm run build
# Watch mode for development
npm run dev📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
🙏 Acknowledgments
- Built for Angular 19+ with modern signal-based architecture
- Inspired by the need for efficient HTTP data management in Angular applications
- Thanks to the Angular team for the excellent HttpClient and Signals APIs
Made with ❤️ for the Angular community
If you find this package useful, please consider giving it a ⭐ on GitHub!
