Invalidation
Auto-invalidate queries after mutations
The invalidation plugin automatically refreshes related queries after mutations succeed using wildcard pattern matching. 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 spoosh = new Spoosh<ApiSchema, Error>("/api").use([
cachePlugin({ staleTime: 5000 }),
deduplicationPlugin(),
invalidationPlugin(),
]);Configuration
// Default: auto-invalidate using [firstSegment, firstSegment/*]
invalidationPlugin(); // same as { autoInvalidate: true }
// Disable auto-invalidation (manual only)
invalidationPlugin({ autoInvalidate: false });
// Groups: use deeper segment matching for grouped endpoints
invalidationPlugin({
groups: ["admin", "api/v1"],
});Groups Configuration
Use groups when you have path prefixes that should be treated as a namespace. This is useful for admin panels, API versioning, or any grouped endpoints:
invalidationPlugin({
groups: ["admin"],
});
// Without groups:
// POST admin/posts → invalidates ["admin", "admin/*"]
// This would invalidate ALL admin queries (admin/users, admin/settings, etc.)
// With groups: ["admin"]:
// POST admin/posts → invalidates ["admin/posts", "admin/posts/*"]
// POST admin/users → invalidates ["admin/users", "admin/users/*"]
// Only the relevant admin sub-resource is invalidatedMulti-segment groups are also supported:
invalidationPlugin({
groups: ["api/v1", "api/v2"],
});
// POST api/v1/users → invalidates ["api/v1/users", "api/v1/users/*"]
// POST api/v2/users → invalidates ["api/v2/users", "api/v2/users/*"]How It Works
Tags are automatically generated from the resolved API path:
useRead((api) => api("posts").GET());
// → tag: "posts"
useRead((api) => api("posts/:id").GET({ params: { id: 123 } }));
// → tag: "posts/123"
useRead((api) => api("posts/:id/comments").GET({ params: { id: 123 } }));
// → tag: "posts/123/comments"Custom Tags on Queries
You can provide custom tags to override the auto-generated tag:
// Custom tag - replaces auto-generated tag
const { data } = useRead((api) => api("users").GET(), {
tags: "my-users",
});
// → tag: "my-users"
// Multiple custom tags
const { data } = useRead((api) => api("users").GET(), {
tags: ["users", "dashboard"],
});
// → tags: ["users", "dashboard"]When a mutation succeeds, related queries are automatically invalidated using wildcard patterns:
const { trigger } = useWrite((api) => api("posts/:id/comments").POST());
await trigger({ params: { id: 123 }, body: { text: "Hello" } });
// Default behavior (autoInvalidate: true):
// Invalidates: ["posts", "posts/*"]
// ✓ Matches: "posts", "posts/123", "posts/123/comments", etc.Pattern Matching
| Pattern | Matches | Does NOT Match |
|---|---|---|
"posts" | "posts" (exact only) | "posts/1", "users" |
"posts/*" | "posts/1", "posts/1/comments" | "posts" (parent) |
["posts", "posts/*"] | "posts" AND all children | - |
Per-Request Invalidation
Override the default invalidation behavior for specific mutations:
const { trigger } = useWrite((api) => api("posts").POST());
// Exact match only
await trigger({
body: { title: "New Post" },
invalidate: "posts",
});
// Children only (not the parent)
await trigger({
body: { title: "New Post" },
invalidate: "posts/*",
});
// Parent AND all children
await trigger({
body: { title: "New Post" },
invalidate: ["posts", "posts/*"],
});
// Multiple patterns
await trigger({
body: { title: "New Post" },
invalidate: ["posts", "users/*", "dashboard"],
});
// Disable invalidation for this mutation
await trigger({
body: { title: "New Post" },
invalidate: false,
});
// Global refetch - triggers ALL queries to refetch
await trigger({
body: { title: "New Post" },
invalidate: "*",
});
// Combined with clearCache (from @spoosh/plugin-cache)
await trigger({
clearCache: true, // Clear all cached data
invalidate: "*", // Then refetch all queries
});Options
Plugin Config
| Option | Type | Default | Description |
|---|---|---|---|
autoInvalidate | boolean | true | Auto-generate invalidation patterns from path |
groups | string[] | [] | Path prefixes that use deeper segment matching |
Per-Request Options
| Option | Type | Description |
|---|---|---|
invalidate | string | string[] | false | "*" | Pattern(s) to invalidate, false to disable, or "*" for global refetch |
Default Behavior
When autoInvalidate: true (default) and no invalidate option is provided:
// POST /posts/123/comments
// → Invalidates: ["posts", "posts/*"]
// The first path segment is used to generate patterns:
// - "posts" - exact match for the root
// - "posts/*" - all children under postsExamples
Wildcard Pattern Invalidation
// Creating a new post
const { trigger: createPost } = useWrite((api) => api("posts").POST());
await createPost({
body: { title: "New Post" },
invalidate: ["posts", "posts/*"],
});
// Invalidates: "posts" and all posts/*, matching posts/1, posts/2, etc.
// Updating a specific post - invalidate only children
const { trigger: updatePost } = useWrite((api) => api("posts/:id").PATCH());
await updatePost({
params: { id: 1 },
body: { title: "Updated" },
invalidate: "posts/*",
});
// Invalidates: posts/1, posts/1/comments, etc. (NOT "posts" itself)
// Delete all comments for a post
const { trigger: deleteComments } = useWrite((api) =>
api("posts/:postId/comments").DELETE()
);
await deleteComments({
params: { postId: 5 },
invalidate: ["posts/5/comments", "posts/5/comments/*"],
});
// Invalidates: posts/5/comments and all its childrenExact vs Wildcard
const { trigger } = useWrite((api) => api("posts/:id/comments").POST());
// Exact match - only invalidates queries with tag "posts"
await trigger({
params: { id: 1 },
body: { text: "Great post!" },
invalidate: "posts",
});
// ✓ Matches: "posts"
// ✗ Does NOT match: "posts/1", "posts/1/comments"
// Wildcard - invalidates all children
await trigger({
params: { id: 1 },
body: { text: "Great post!" },
invalidate: "posts/*",
});
// ✓ Matches: "posts/1", "posts/1/comments", "posts/2"
// ✗ Does NOT match: "posts"
// Both - invalidates parent and all children
await trigger({
params: { id: 1 },
body: { text: "Great post!" },
invalidate: ["posts", "posts/*"],
});
// ✓ Matches: "posts", "posts/1", "posts/1/comments", "posts/2"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 { create } from "@spoosh/react";
const { useRead, invalidate } = create(spoosh);
// Single pattern
invalidate("posts");
// Multiple patterns
invalidate(["posts", "users/*"]);
// Global refetch - triggers ALL queries to refetch
invalidate("*");WebSocket Example
import { useEffect } from "react";
import { invalidate } from "./spoosh";
function Dashboard() {
useEffect(() => {
socket.on("posts-updated", () => {
invalidate(["posts", "posts/*"]);
});
socket.on("full-sync", () => {
invalidate("*");
});
return () => {
socket.off("posts-updated");
socket.off("full-sync");
};
}, []);
return <div>...</div>;
}Combining with Cache Plugin
For scenarios like logout or user switching, combine invalidate: "*" with clearCache from @spoosh/plugin-cache:
const { trigger } = useWrite((api) => api("auth/logout").POST());
// Clear cache + trigger all queries to refetch
await 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 spoosh = new Spoosh<ApiSchema, Error>("/api").use([
cachePlugin({ staleTime: 5000 }),
deduplicationPlugin(),
invalidationPlugin(),
]);