A lightweight, type-safe library for effortless data mutations with SWR
SWR excels at data fetching, but mutations (create, update, delete) require significant boilerplate—especially with optimistic updates. swr-catalyst eliminates this repetition with declarative hooks that handle:
- ✅ Optimistic UI updates with automatic rollback
- ✅ Loading and error states out of the box
- ✅ Type-safe mutations with full TypeScript support
- ✅ Advanced cache management utilities
- ✅ Zero configuration required
- 🎯 Three core hooks:
useSWRCreate,useSWRUpdate,useSWRDelete - 🚀 Optimistic updates: Instant UI feedback with automatic error rollback
- 📦 Tiny footprint: ~3KB minified + gzipped
- 🔒 Type-safe: Full TypeScript support with generics
- 🛠️ Cache utilities: Batch operations with
mutateById,mutateByGroup,resetCache - 🔑 Structured keys: Uses typed
SWRKeyobjects for enhanced cache management - ⚡ Smart error handling: Custom
MutationErrorclass with helpful context
npm install swr-catalystpnpm add swr-catalystyarn add swr-catalystNote: This library requires
reactandswras peer dependencies. Make sure you have them installed in your project.
import { useSWRCreate, useSWRUpdate, useSWRDelete } from 'swr-catalyst';
// Create
const { trigger: createTodo, isMutating, error } = useSWRCreate(
{ id: 'todos', data: '/api/todos' },
async (newTodo) => api.post('/todos', newTodo)
);
await createTodo({ title: 'Buy milk' });
// Update
const { trigger: updateTodo } = useSWRUpdate(
{ id: 'todos', data: '/api/todos' },
async (id, data) => api.patch(`/todos/${id}`, data)
);
await updateTodo(1, { completed: true });
// Delete
const { trigger: deleteTodo } = useSWRDelete(
{ id: 'todos', data: '/api/todos' },
async (id) => api.delete(`/todos/${id}`)
);
await deleteTodo(1);Make your UI feel instant with optimistic updates:
import { useSWRCreate } from 'swr-catalyst';
const { trigger: addTodo } = useSWRCreate(
{ id: 'todos', data: '/api/todos' },
createTodoAPI,
{
optimisticUpdate: (currentTodos, newTodo) => [
...(currentTodos || []),
{ ...newTodo, id: `temp-${Date.now()}` }
],
rollbackOnError: true // Automatically reverts on failure
}
);
// UI updates immediately, syncs with server in background
await addTodo({ title: 'New task' });import { useSWRCreate } from 'swr-catalyst';
function AddTodoForm() {
const { trigger: addTodo, isMutating, error } = useSWRCreate(
{ id: 'todos', data: '/api/todos' },
createTodoAPI
);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
try {
await addTodo({ title: formData.get('title') });
e.currentTarget.reset();
} catch (err) {
// Error state is automatically updated
}
};
return (
<form onSubmit={handleSubmit}>
<input name="title" type="text" disabled={isMutating} />
<button type="submit" disabled={isMutating}>
{isMutating ? 'Adding...' : 'Add Todo'}
</button>
{error && <p className="error">Failed to add todo: {error.message}</p>}
</form>
);
}All hooks return a custom MutationError when mutations fail. This error class provides rich context and helpful methods for handling failures gracefully.
name: Always"MutationError"message: Human-readable error descriptioncontext: Object containing mutation details:operation: The type of mutation ("create","update", or"delete")key: TheSWRKeythat was being mutateddata: The payload data (for create/update operations)id: The resource ID (for update/delete operations)timestamp: When the error occurred (milliseconds since epoch)
originalError: The underlying error that caused the failurestack: Error stack trace
getUserMessage(): string
Returns a user-friendly error message suitable for displaying in UI.
const error = mutationError.getUserMessage();
// Returns: "Failed to add todos. Please try again."isNetworkError(): boolean
Checks if the error was caused by network-related issues (connection failures, timeouts, etc.).
if (error.isNetworkError()) {
toast.error('Network error. Check your connection and try again.', {
action: { label: 'Retry', onClick: handleRetry }
});
}isValidationError(): boolean
Checks if the error was caused by validation failures (invalid data, required fields, etc.).
if (error.isValidationError()) {
// Show validation-specific message
toast.error('Please check your input and try again.');
} else {
// Show generic error with retry option
toast.error('Something went wrong', { action: 'Retry' });
}toJSON(): object
Serializes the error to a JSON-compatible object for logging and error tracking services.
// Send to error tracking service
Sentry.captureException(mutationError.toJSON());
// Log for debugging
console.error('Mutation failed:', mutationError.toJSON());import { useSWRCreate, MutationError } from 'swr-catalyst';
function TodoForm() {
const { trigger, error } = useSWRCreate(
{ id: 'todos', data: '/api/todos' },
createTodoAPI
);
const handleSubmit = async (data) => {
try {
await trigger(data);
} catch (err) {
if (err instanceof MutationError) {
// Use helper methods for better UX
if (err.isNetworkError()) {
toast.error('Network error. Please check your connection.');
} else if (err.isValidationError()) {
toast.error(err.getUserMessage());
} else {
// Generic error handling
toast.error('Something went wrong. Please try again.');
}
// Log detailed error for debugging
console.error('Mutation context:', err.context);
console.error('Original error:', err.originalError);
}
}
};
// Or use the error state directly
if (error) {
return <Alert>{error.getUserMessage()}</Alert>;
}
return <form onSubmit={handleSubmit}>...</form>;
}Important: swr-catalyst requires a specific key structure for all hooks and utilities to work properly.
type SWRKey<T = unknown> = {
id: string; // Required: Unique identifier for the cache entry
group?: string; // Optional: Group name for batch operations
data: T; // Required: The actual SWR key (URL, array, etc.)
} | null;The structured key format enables powerful cache management features:
id: AllowsmutateById()to update specific cache entriesgroup: EnablesmutateByGroup()to batch-update related cachesdata: The actual key passed to SWR's fetcher function
The library handles key stability internally using deep equality comparison, so you don't need to wrap the key object yourself. The useStableKey hook automatically detects when values change, even for complex nested objects.
// ✅ Simple primitive value for data
const { trigger: createTodo } = useSWRCreate(
{ id: 'todos', data: '/api/todos' },
createTodoAPI
);
// ✅ Complex object data - no useMemo needed!
// The library handles deep equality automatically
const { trigger: createTodo } = useSWRCreate(
{ id: 'todos', data: { url: '/api/todos', userId: user.id } },
createTodoAPI
);
// ✅ Even deeply nested objects work automatically
const { trigger: createTodo } = useSWRCreate(
{
id: 'todos',
data: {
url: '/api/todos',
params: { userId: user.id, filter: 'active' }
}
},
createTodoAPI
);This library exports a set of hooks and utility functions to streamline your data mutation workflow.
A hook for creating new data. It handles optimistic updates, loading states, and revalidates the cache upon success.
key: ASWRKeyobject that will be revalidated after creation.createFunction: An async function(data) => Promise<NewData>that performs the API call.options(optional): Configuration for optimistic updates (optimisticUpdate,rollbackOnError).
Returns: { trigger, isMutating, error }
A hook for updating existing data.
key: ASWRKeyobject that will be revalidated after the update.updateFunction: An async function(id, data) => Promise<UpdatedData>that performs the API call.options(optional): Configuration for optimistic updates.
Returns: { trigger, isMutating, error }
A hook for deleting data.
key: ASWRKeyobject that will be revalidated after deletion.deleteFunction: An async function(id) => Promise<void>that performs the API call.options(optional): Configuration for optimistic updates.
Returns: { trigger, isMutating, error }
A utility hook that memoizes an SWRKey using deep equality comparison. This prevents unnecessary re-renders in child components when a key's object reference changes but its values do not. It is used internally by all mutation hooks.
key: TheSWRKeyobject to stabilize.
Returns: A memoized SWRKey with a stable reference.
Mutates all SWR cache entries whose key id matches one or more provided IDs.
ids: A single ID string or an array of IDs.newData(optional): The new data to set for the matched keys. If omitted, matching keys are revalidated.options(optional): SWR mutator options (revalidate,populateCache, etc.).
Example:
// Revalidate all caches related to 'user' and 'profile'
await mutateById(['user', 'profile']);Mutates all SWR cache entries whose key group matches one or more provided group names.
groups: A single group string or an array of groups.newData(optional): The new data to set for the matched keys. If omitted, matching keys are revalidated.options(optional): SWR mutator options.
Example:
// Update all caches in the 'user-data' group without revalidating
await mutateByGroup('user-data', updatedData, { revalidate: false });Clears the entire SWR cache, with an option to preserve specific entries by their id.
preservedKeys(optional): A single ID string or an array of IDs to exclude from the reset.
Example:
// On logout, clear all data except for public content
await resetCache(['public-posts', 'app-config']);Wraps any promise and converts it into a [data, error] tuple, inspired by Go's error handling style. This avoids try/catch blocks for cleaner async code.
- Returns: A promise that always resolves with
[data, null]on success or[null, error]on failure.
Example:
import { to } from 'swr-catalyst';
const [result, error] = await to(createTodo({ title: 'New item' }));
if (error) {
console.error('Mutation failed:', error);
}A type-safe wrapper around SWR's mutate function that correctly handles the structured SWRKey object.
mutate: Themutatefunction fromuseSWRConfig().key: TheSWRKeyto mutate.data(optional): The new data to update the cache with.shouldRevalidate(optional): Whether to revalidate after mutation.
Example:
// Perform an optimistic update without revalidation
await swrMutate(mutate, key, optimisticData, false);A type-safe wrapper around SWR's cache.get() method that correctly handles the structured SWRKey object.
cache: Thecacheobject fromuseSWRConfig().key: TheSWRKeyto look up.
Returns: The cached data or undefined.
Example:
const { cache } = useSWRConfig();
const cachedTodos = swrGetCache(cache, { id: 'todos', data: '/api/todos' });| Package | Minified | Minified + Gzipped |
|---|---|---|
| swr-catalyst | ~11.7KB | ~3.2KB |
Zero dependencies beyond peer dependencies (react and swr).
Contributions are welcome! Please read our Contributing Guide for details.
MIT © Pedro Barbosa