Spoosh
Guides

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:

PatternMatchesDoes 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:

PatternEffect
Default (autoInvalidate: true)[firstSegment, firstSegment/*]
"posts"Exact match only
"posts/*"All children of posts
["posts", "posts/*"]posts AND all children
falseSkip 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

ScenarioPattern
Standard CRUDDefault (auto-invalidate)
Specific resource update"posts/123"
All children of a resource"posts/*"
Resource and all children["posts", "posts/*"]
No related queries to refreshinvalidate: false
User logout / account switchinvalidate: "*" + 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.

On this page