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"
useRead((api) => api("users/:id/posts").GET({ params: { id: 123 } }));
// When this mutation succeeds...
const { trigger } = useWrite((api) => api("users/:id").PATCH());
await 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
useRead((api) => api("dashboard/stats").GET(), {
tags: "dashboard",
});
// Multiple custom tags
useRead((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 |
const { trigger } = useWrite((api) => api("posts").POST());
// Default behavior — invalidates ["posts", "posts/*"]
await trigger({ body: newPost });
// Exact match only
await trigger({ body: newPost, invalidate: "posts" });
// Children only
await trigger({ body: newPost, invalidate: "posts/*" });
// Parent and children
await trigger({ body: newPost, invalidate: ["posts", "posts/*"] });
// Cross-domain — refresh unrelated queries too
await trigger({ body: newPost, invalidate: ["posts", "posts/*", "dashboard"] });
// Disable invalidation
await trigger({ body: newPost, invalidate: false });Manual Invalidation
For events outside mutations (WebSocket, timers, external state), trigger invalidation manually:
import { invalidate } from "./spoosh";
// Invalidate specific patterns
socket.on("posts-updated", () => {
invalidate(["posts", "posts/*"]);
});
// Force everything to refresh
socket.on("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.