Invalidation
Auto-invalidate queries after mutations
The invalidation plugin automatically refreshes related queries after mutations succeed. This keeps your UI in sync without manual refetching.
Installation
npm install @spoosh/plugin-invalidationUsage
import { Spoosh } from "@spoosh/core";
import { cachePlugin } from "@spoosh/plugin-cache";
import { deduplicationPlugin } from "@spoosh/plugin-deduplication";
import { invalidationPlugin } from "@spoosh/plugin-invalidation";
const client = new Spoosh<ApiSchema, Error>("/api").use([
cachePlugin({ staleTime: 5000 }),
deduplicationPlugin(),
invalidationPlugin(),
]);Default Configuration
// Default: invalidate all related tags (full hierarchy)
invalidationPlugin(); // same as { defaultMode: "all" }
// Only invalidate the exact endpoint by default
invalidationPlugin({ defaultMode: "self" });
// Disable auto-invalidation by default (manual only)
invalidationPlugin({ defaultMode: "none" });How It Works
Tags are automatically generated from the API path hierarchy:
// Query tags are generated from the path:
injectRead((api) => api("users").GET());
// → tags: ["users"]
injectRead((api) => api("users/:id").GET({ params: { id: 123 } }));
// → tags: ["users", "users/123"]
injectRead((api) => api("users/:id/posts").GET({ params: { id: 123 } }));
// → tags: ["users", "users/123", "users/123/posts"]Custom Tags on Queries
You can override or extend auto-generated tags using the unified tags option:
// Mode only - 'all' generates full hierarchy
posts = injectRead(
(api) => api("users/:id/posts").GET({ params: { id: "123" } }),
{
tags: "all", // ['users', 'users/123', 'users/123/posts']
}
);
// Mode only - 'self' generates only exact path
posts = injectRead(
(api) => api("users/:id/posts").GET({ params: { id: "123" } }),
{
tags: "self", // ['users/123/posts']
}
);
// Custom tags only - replaces auto-generated tags
users = injectRead((api) => api("users").GET(), {
tags: ["custom-users", "dashboard"], // ['custom-users', 'dashboard']
});
// Mode + custom tags - 'all' mode combined with custom tags
posts = injectRead(
(api) => api("users/:id/posts").GET({ params: { id: "123" } }),
{
tags: ["all", "dashboard"], // ['users', 'users/123', 'users/123/posts', 'dashboard']
}
);
// Mode + custom tags - 'self' mode combined with custom tags
posts = injectRead(
(api) => api("users/:id/posts").GET({ params: { id: "123" } }),
{
tags: ["self", "dashboard"], // ['users/123/posts', 'dashboard']
}
);When a mutation succeeds, related queries are automatically invalidated:
createPost = injectWrite((api) => api("users/:id/posts").POST);
await createPost.trigger({ params: { id: 123 }, body: { title: "New Post" } });
// ✓ Invalidates: "users", "users/123", "users/123/posts"
// All queries matching these tags will refetch automaticallyPer-Request Invalidation
Override the default invalidation behavior for specific mutations:
createPost = injectWrite((api) => api("posts").POST);
// Mode only (string)
await createPost.trigger({
body: { title: "New Post" },
invalidate: "all", // Invalidate entire path hierarchy
});
await createPost.trigger({
body: { title: "New Post" },
invalidate: "self", // Only invalidate the exact endpoint
});
await createPost.trigger({
body: { title: "New Post" },
invalidate: "none", // No invalidation
});
// Single tag (string)
await createPost.trigger({
body: { title: "New Post" },
invalidate: "posts", // Invalidate only "posts" tag
});
// Multiple tags (array without mode keyword)
await createPost.trigger({
body: { title: "New Post" },
invalidate: ["posts", "users", "custom-tag"],
// → Only explicit tags are invalidated
});
// Mode + Tags (array with mode keyword)
await createPost.trigger({
body: { title: "New Post" },
invalidate: ["all", "dashboard", "stats"],
// → 'all' mode + explicit tags
});
await createPost.trigger({
body: { title: "New Post" },
invalidate: ["posts", "self", "users"],
// → 'self' mode + explicit tags
});
// Wildcard - global refetch
await createPost.trigger({
body: { title: "New Post" },
invalidate: "*", // Triggers ALL queries to refetch
});
// Combined with clearCache (from @spoosh/plugin-cache)
await createPost.trigger({
clearCache: true, // Clear all cached data
invalidate: "*", // Then refetch all queries
});Options
Plugin Config
| Option | Type | Default | Description |
|---|---|---|---|
defaultMode | "all" | "self" | "none" | "all" | Default invalidation mode when option not specified |
Per-Request Options
| Option | Type | Description |
|---|---|---|
invalidate | "all" | "self" | "none" | "*" | string | string[] | Mode ("all", "self", "none"), wildcard ("*" for global refetch), single tag, or array of tags with optional mode keyword |
Invalidation Modes
| Mode | Description |
|---|---|
"all" | Invalidate all tags from path hierarchy (default) |
"self" | Only invalidate the exact endpoint tag |
"none" | Disable auto-invalidation (manual only) |
"*" | Global refetch - triggers all queries to refetch |
Understanding "all" vs "*"
These two options serve different purposes:
-
"all"- Invalidates all tags from the current endpoint's path hierarchy. If you're mutatingusers/123/posts, it invalidates["users", "users/123", "users/123/posts"]. It's scoped to the mutation's path. -
"*"- Triggers a global refetch of every active query in your app, regardless of tags. Use this sparingly for scenarios like "user logged out" or "full data sync from server".
createPost = injectWrite((api) => api("users/:id/posts").POST);
// "all" - scoped to this mutation's path hierarchy
await createPost.trigger({ invalidate: "all" });
// If path is users/123/posts → invalidates: users, users/123, users/123/posts
// "*" - refetches ALL queries in the entire app
await createPost.trigger({ invalidate: "*" });
// Every active injectRead will refetchExamples
Nested Path Invalidation
// Creating a new post
createPost = injectWrite((api) => api("posts").POST);
await createPost.trigger({
body: { title: "New Post" },
invalidate: "all",
});
// Invalidates: ["posts"]
// Updating a specific post
updatePost = injectWrite((api) => api("posts/:id").PATCH);
const postId = 1;
await updatePost.trigger({
params: { id: postId },
body: { title: "Updated" },
invalidate: "all",
});
// Invalidates: ["posts", `posts/${postId}`]
// Deleting a comment on a post
deleteComment = injectWrite((api) => api("posts/:postId/comments/:id").DELETE);
const postId = 5;
const commentId = 10;
await deleteComment.trigger({
params: { postId, id: commentId },
invalidate: "all",
});
// Invalidates: ["posts", `posts/${postId}`, `posts/${postId}/comments`, `posts/${postId}/comments/${commentId}`]Mode Comparison
// Path: posts/:id/comments
createComment = injectWrite((api) => api("posts/:id/comments").POST);
const postId = 1;
// Mode "all" invalidates full hierarchy
await createComment.trigger({
params: { id: postId },
body: { text: "Great post!" },
invalidate: "all",
});
// Invalidates: ["posts", `posts/${postId}`, `posts/${postId}/comments`]
// Mode "self" invalidates only exact endpoint
await createComment.trigger({
params: { id: postId },
body: { text: "Great post!" },
invalidate: "self",
});
// Invalidates: [`posts/${postId}/comments`]
// Mode "none" invalidates nothing
await createComment.trigger({
params: { id: postId },
body: { text: "Great post!" },
invalidate: "none",
});
// Invalidates: []Combining Mode with Explicit Tags
createComment = injectWrite((api) => api("posts/:id/comments").POST);
const postId = 1;
// Invalidate hierarchy + specific tags
await createComment.trigger({
params: { id: postId },
body: { text: "Great post!" },
invalidate: ["all", "dashboard", "user-stats"],
});
// Invalidates: ["posts", `posts/${postId}`, `posts/${postId}/comments`, "dashboard", "user-stats"]
// Invalidate self + specific tags
await createComment.trigger({
params: { id: postId },
body: { text: "Great post!" },
invalidate: ["self", `posts/${postId}`, "dashboard"],
});
// Invalidates: [`posts/${postId}/comments`, `posts/${postId}`, "dashboard"]Manual Invalidation
The plugin exposes invalidate for manually triggering cache invalidation outside of mutations. This is useful for external events like WebSocket messages or other state changes.
import { createAngularSpoosh } from "@spoosh/angular";
const { injectRead, invalidate } = createAngularSpoosh(client);
// Invalidate with string array
invalidate(["users", "posts"]);
// Invalidate with single string
invalidate("posts");
// Global refetch - triggers ALL queries to refetch
invalidate("*");WebSocket Example
import { Component, OnDestroy, OnInit, inject } from "@angular/core";
import { invalidate } from "./spoosh";
@Component({
selector: "app-dashboard",
template: `<div>...</div>`,
})
export class DashboardComponent implements OnInit, OnDestroy {
ngOnInit() {
socket.on("data-changed", (tags: string[]) => {
invalidate(tags);
});
// Trigger global refetch on full sync
socket.on("full-sync", () => {
invalidate("*");
});
}
ngOnDestroy() {
socket.off("data-changed");
socket.off("full-sync");
}
}Combining with Cache Plugin
For scenarios like logout or user switching, combine invalidate: "*" with clearCache from @spoosh/plugin-cache:
logout = injectWrite((api) => api("auth/logout").POST);
// Clear cache + trigger all queries to refetch
await logout.trigger({
clearCache: true, // From cache plugin: clear all cached data
invalidate: "*", // From invalidation plugin: trigger all queries to refetch
});This ensures both:
- All cached data is cleared (no stale data from previous session)
- All active queries refetch with fresh data
Combining with Deduplication
When invalidation triggers multiple queries to refetch, some may share the same endpoint. Use deduplicationPlugin to prevent duplicate network requests:
import { Spoosh } from "@spoosh/core";
import { cachePlugin } from "@spoosh/plugin-cache";
import { deduplicationPlugin } from "@spoosh/plugin-deduplication";
import { invalidationPlugin } from "@spoosh/plugin-invalidation";
const client = new Spoosh<ApiSchema, Error>("/api").use([
cachePlugin({ staleTime: 5000 }),
deduplicationPlugin(),
invalidationPlugin(),
]);