Tags & Invalidation
Understand how queries are tagged and when they refresh
Spoosh uses tags to determine which queries should refresh after a mutation. Understanding this system helps you control cache behavior effectively.
This guide covers the Invalidation Plugin. See the plugin page for installation and configuration.
The Mental Model
Every query is automatically tagged based on its resolved API path. When a mutation succeeds, Spoosh uses wildcard pattern matching to find and refetch matching queries.
// This query gets tagged with: "users/123/posts"
injectRead((api) => api("users/:id/posts").GET({ params: { id: 123 } }));
// When this mutation succeeds...
writer = injectWrite((api) => api("users/:id").PATCH());
await writer.trigger({ params: { id: 123 }, body: updated });
// → Default invalidation: ["users", "users/*"]
// → The query above refetches because "users/123/posts" matches "users/*"Pattern Matching
Invalidation uses wildcard patterns to match query tags:
| Pattern | Matches | Does NOT Match |
|---|---|---|
"posts" | "posts" (exact) | "posts/1" |
"posts/*" | "posts/1", "posts/1/comments" | "posts" |
["posts", "posts/*"] | "posts" AND all children | - |
Custom Tags
Override the auto-generated tag with custom tags:
// Single custom tag
injectRead((api) => api("dashboard/stats").GET(), {
tags: "dashboard",
});
// Multiple custom tags
injectRead((api) => api("users/:id/posts").GET({ params: { id: "5" } }), {
tags: ["user-posts", "dashboard"],
});Invalidation Patterns
After a mutation, control what gets invalidated:
| Pattern | Effect |
|---|---|
Default (autoInvalidate: true) | [firstSegment, firstSegment/*] |
"posts" | Exact match only |
"posts/*" | All children of posts |
["posts", "posts/*"] | posts AND all children |
false | Skip invalidation |
"*" | Global refetch — every active query refetches |
postWriter = injectWrite((api) => api("posts").POST());
// Default behavior — invalidates ["posts", "posts/*"]
await postWriter.trigger({ body: newPost });
// Exact match only
await postWriter.trigger({ body: newPost, invalidate: "posts" });
// Children only
await postWriter.trigger({ body: newPost, invalidate: "posts/*" });
// Parent and children
await postWriter.trigger({ body: newPost, invalidate: ["posts", "posts/*"] });
// Cross-domain — refresh unrelated queries too
await postWriter.trigger({
body: newPost,
invalidate: ["posts", "posts/*", "dashboard"],
});
// Disable invalidation
await postWriter.trigger({ body: newPost, invalidate: false });Manual Invalidation
For events outside mutations (WebSocket, timers, external state), trigger invalidation manually:
import { invalidate } from "./spoosh";
// From a WebSocket handler
this.ws.onmessage = (event) => {
const { type, patterns } = JSON.parse(event.data);
if (type === "invalidate") {
invalidate(patterns);
}
if (type === "full-sync") {
invalidate("*");
}
};Choosing the Right Pattern
| Scenario | Pattern |
|---|---|
| Standard CRUD | Default (auto-invalidate) |
| Specific resource update | "posts/123" |
| All children of a resource | "posts/*" |
| Resource and all children | ["posts", "posts/*"] |
| No related queries to refresh | invalidate: false |
| User logout / account switch | invalidate: "*" + clearCache: true |
| Cross-domain updates | ["posts", "dashboard"] |
| External events (WebSocket) | invalidate(patterns) manually |
For complete API reference, configuration options, and advanced examples, see the Invalidation Plugin.