Spoosh
Plugins

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-invalidation

Usage

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 invalidated

Multi-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

PatternMatchesDoes 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

OptionTypeDefaultDescription
autoInvalidatebooleantrueAuto-generate invalidation patterns from path
groupsstring[][]Path prefixes that use deeper segment matching

Per-Request Options

OptionTypeDescription
invalidatestring | 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 posts

Examples

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 children

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

  1. All cached data is cleared (no stale data from previous session)
  2. 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(),
]);

On this page