Caching is nut a problem!
β
HTTP Caching
β
State Management Mode
β
Local Storage Support
β
Session Storage Support
β
Dynamic Storage Strategy
β
Handles Simultaneous Requests
β
Automatic & Manual Cache Busting
β
Hackable
A flexible and straightforward library that caches HTTP requests in Angular
$ npm install @ngneat/cashewUse the provideHttpCache provider along with withHttpCacheInterceptor in your application providers:
import { provideHttpCache, withHttpCacheInterceptor } from '@ngneat/cashew';
bootstrapApplication(AppComponent, {
providers: [provideHttpClient(withInterceptors([withHttpCacheInterceptor()])), provideHttpCache()]
});And you're done! Now, when using Angular HttpClient, you can pass the withCache function as context, and it'll cache the response:
import { withCache } from '@ngneat/cashew';
@Injectable()
export class UsersService {
constructor(private http: HttpClient) {}
getUsers() {
return this.http.get('api/users', {
context: withCache()
});
}
}It's as simple as that.
When working with state management like Akita or ngrx, there is no need to save the data both in the cache and in the store because the store is the single source of truth. In such a case, the only thing we want is an indication of whether the data is in the cache.
We can change the mode option to stateManagement:
import { withCache } from '@ngneat/cashew';
@Injectable()
export class UsersService {
constructor(private http: HttpClient) {}
getUsers() {
return this.http.get('api/users', {
context: withCache({
mode: 'stateManagement'
})
});
}
}Now instead of saving the actual response in the cache, it'll save a boolean and will return by default an EMPTY observable when the boolean resolves to true. You can change the returned source by using the returnSource option.
By default, caching is done to app memory. To switch to using, for instance, local storage instead simply add withLocalStorage():
Keep in mind that the local storage has a size limit of 5-10MB depending on the browser, so it is not recommended to cache large responses.
import { provideHttpCache, withHttpCacheInterceptor, withLocalStorage } from '@ngneat/cashew';
bootstrapApplication(AppComponent, {
providers: [provideHttpClient(withInterceptors([withHttpCacheInterceptor()])), provideHttpCache(withLocalStorage())]
});and then configure your requests to use the local storage strategy:
getUsers() {
return this.http.get('api/users', {
context: withCache({
storage: 'localStorage'
})
});
}Note that ttl will also be calculated via local storage in this instance.
You can also use session storage by using withSessionStorage() and storage: 'sessionStorage' in the withCache function:
import { provideHttpCache, withHttpCacheInterceptor, withSessionStorage } from '@ngneat/cashew';
bootstrapApplication(AppComponent, {
providers: [provideHttpClient(withInterceptors([withHttpCacheInterceptor()])), provideHttpCache(withSessionStorage())]
});getUsers() {
return this.http.get('api/users', {
context: withCache({
storage: 'sessionStorage'
})
});
}When working with localStorage or sessionStorage, it's recommended to add a version:
import { withCache } from '@ngneat/cashew';
@Injectable()
export class UsersService {
constructor(private http: HttpClient) {}
getUsers() {
return this.http.get('api/users', {
context: withCache({
storage: 'localStorage', // or 'sessionStorage'
version: 'v1',
key: 'users'
})
});
}
}When you have a breaking change, change the version, and it'll delete the current cache automatically.
Using the library, you might need to change the default behavior of the caching mechanism. You could do that by passing a configuration to the provideHttpCache function as first argument:
bootstrapApplication(AppComponent, {
providers: [provideHttpClient(withInterceptors([withHttpCacheInterceptor()])), provideHttpCache(config)]
});bootstrapApplication(AppComponent, {
providers: [provideHttpClient(withInterceptors([withHttpCacheInterceptor()])), provideHttpCache(config, withLocalStorage())]
});Or by using the withCache function:
import { withCache } from '@ngneat/cashew';
```ts
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptors([withHttpCacheInterceptor()])),
provideHttpCache(
withConfig({
responseSerializer(value) {
return structuredClone(value);
}
})
),
]
});Let's go over each of the configuration options:
Defines the caching behavior. The library supports two different strategies:
explicit(default) - only caches API requests that explicitly use thewithCachefunctionimplicit- caches API requests that are of typeGETand the response type isJSON. You can change this behavior by overriding theHttpCacheGuardprovider. (See the Hackable section)
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptors([withHttpCacheInterceptor()])),
provideHttpCache({ strategy: 'implicit' })
]
});Define the cache TTL (time to live) in milliseconds: (defaults to one hour)
bootstrapApplication(AppComponent, {
providers: [provideHttpClient(withInterceptors([withHttpCacheInterceptor()])), provideHttpCache({ ttl: number })]
});By default, the registry returns the original response object. It can be dangerous if, for some reason, you mutate it. To change this behavior, you can clone the response before getting it:
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptors([withHttpCacheInterceptor()])),
provideHttpCache({
responseSerializer(body) {
return cloneDeep(body);
}
})
]
});You can also provide additional providers to the provideHttpCache function. For example, if you want to use a custom storage strategy:
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptors([withHttpCacheInterceptor()])),
provideHttpCache(withLocalStorage(), withSessionStorage())
]
});Currently, there is no way in Angular to pass metadata to an interceptor. The withCache function uses the params object to pass the config and removes it afterward in the interceptor. The function receives four optional params that are postfixed with a $ sign so it'll not conflicts with others:
cache- Whether to cache the request (defaults totrue)ttl- TTL that will override the globalkey- Custom key. (defaults to the request URL including any query params)bucket- The bucket in which we save the keysversion- To use when working withlocalStorage(see Versioning).storage- Cache storage strategy for the request. Can be 'memory' or 'localStorage'. Overrides the global strategy.clearCachePredicate(previousRequest, currentRequest)- Returntrueto clear the cache for this keycontext- Allow chaining function call that returns anHttpContext.
import { requestDataChanged, withCache } from '@ngneat/cashew';
@Injectable()
export class UsersService {
constructor(private http: HttpClient) {}
getUsers() {
return this.http.get('api/users', {
context: withCache({
withCache: false,
ttl: 40000,
key: 'users',
storage: 'localStorage', // <-- Add this line for local storage caching
clearCachePredicate: requestDataChanged
})
});
}
}When you need to call another function that returns an HttpContext, you can provide the context option.
import { withCache } from '@ngneat/cashew';
import { withLoadingSpinner } from '@another/library'; // <-- function that returns an HttpContext
@Injectable()
export class TodosService {
constructor(private http: HttpClient) {}
getTodos() {
return this.http.get('api/todos', {
context: withCache({
context: withLoadingSpinner()
})
});
}
}The CacheManager provider, exposes an API to update and query the cache registry:
get<T>(key: string): HttpResponse<T>- Get theHttpResponsefrom the cachehas(key: string)- Returns abooleanindicates whether the providedkeyexists in the cacheset(key: string, body: any, { ttl, bucket })- Set manually a new entry in the cachedelete(key: string | CacheBucket)- Delete from the cache
CacheBucket can be useful when we need to buffer multiple requests and invalidate them at some point. For example:
import { withCache, CacheBucket } from '@ngneat/cashew';
@Injectable()
export class TodosService {
todosBucket = new CacheBucket();
constructor(
private http: HttpClient,
private manager: HttpCacheManager
) {}
getTodo(id) {
return this.http.get(`todos/${id}`, {
context: withCache({
bucket: this.todosBucket
})
});
}
invalidateTodos() {
this.manager.delete(this.todosBucket);
}
}Now when we call the invalidateTodos method, it'll automatically delete all the ids that it buffered. CacheBucket also exposes the add, has, delete, and clear methods.
HttpCacheStorage- The storage to use: (defaults to in-memory storage)
abstract class HttpCacheStorage {
abstract has(key: string): boolean;
abstract get(key: string): HttpResponse<any>;
abstract set(key: string, response: HttpResponse<any>): void;
abstract delete(key?: string): void;
}KeySerializer- Generate the cache key based on the request: (defaults torequest.urlWithParams)
export abstract class KeySerializer {
abstract serialize(request: HttpRequest): string;
}HttpCacheGuard- When using theimplicitstrategy it first verifies thatcanActivateis truthy:
export abstract class HttpCacheGuard {
abstract canActivate(request: HttpCacheHttpRequestRequest): boolean;
}It defaults to request.method === 'GET' && request.responseType === 'json'.
TTLManager- A class responsible for managing the requests TTL:
abstract class TTLManager {
abstract isValid(key: string): boolean;
abstract set(key: string, ttl?: number): void;
abstract delete(key?: string): void;
}| Cashew | Angular |
|---|---|
| ^4.0.0 | ^17.0.0 |
| 3.1.0 | >13.0.0 < 17 |
| 3.0.0 | ^13.0.0 |
| ^2.0.0 | ^12.0.0 |
| ^1.0.0 | ^10.0.0 |
Thanks go to these wonderful people (emoji key):
Netanel Basal π» π¨ π π€ π |
Itay Oded π» |
Shahar Kazaz π» |
Lars Gyrup Brink Nielsen π |
RaΓ Siqueira π |
Inbal Sinai π» π |
James Manners π» |
mokipedia π» π |
This project follows the all-contributors specification. Contributions of any kind welcome!