npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

ng-memoize-data-storage

v1.0.1

Published

A TypeScript utility library for memoizing data storage in Angular applications with HTTP caching

Downloads

13

Readme

ng-memoize-data-storage

npm version License: MIT TypeScript

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-storage

Peer 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 object
  • multipleData: Signal<T[]> - Read-only signal containing array of data objects
  • isLoading: 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

  1. Reuse Storage Instances: Create storage instances as component properties to maintain cache across component lifecycle.

  2. Strategic Cache Clearing: Only clear cache when necessary (e.g., after data mutations).

  3. Query Parameter Caching: Different query parameters create separate cache entries.

  4. Loading State Optimization: Use the isLoading signal 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!