React Hooks library for remote data fetching with stale-while-revalidate caching strategy
—
The useSWRMutation hook handles remote mutations (POST, PUT, DELETE, PATCH) with optimistic updates, error handling, and cache management.
Hook for handling remote mutations with optimistic updates and rollback support.
/**
* Hook for remote mutations with optimistic updates and rollback support
* @param key - Unique identifier for the mutation
* @param fetcher - Function that performs the mutation
* @param config - Configuration options for the mutation
* @returns SWRMutationResponse with trigger function and mutation state
*/
function useSWRMutation<Data = any, Error = any, ExtraArg = never>(
key: Key,
fetcher: MutationFetcher<Data, Key, ExtraArg>,
config?: SWRMutationConfiguration<Data, Error, Key, ExtraArg>
): SWRMutationResponse<Data, Error, Key, ExtraArg>;Usage Examples:
import useSWRMutation from "swr/mutation";
// Basic mutation
const { trigger, isMutating, data, error } = useSWRMutation(
"/api/user",
async (url, { arg }: { arg: { name: string } }) => {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(arg)
});
return response.json();
}
);
// Trigger the mutation
const handleSubmit = async (formData: { name: string }) => {
try {
const result = await trigger(formData);
console.log("User created:", result);
} catch (error) {
console.error("Failed to create user:", error);
}
};
// Mutation with optimistic updates
const { trigger } = useSWRMutation(
"/api/user/123",
updateUserFetcher,
{
optimisticData: (currentData) => ({ ...currentData, updating: true }),
rollbackOnError: true,
populateCache: true,
revalidate: false,
}
);The return value from useSWRMutation with mutation control and state.
interface SWRMutationResponse<Data, Error, Key, ExtraArg> {
/** The data returned by the mutation (undefined if not triggered or error) */
data: Data | undefined;
/** The error thrown by the mutation (undefined if no error) */
error: Error | undefined;
/** Function to trigger the mutation */
trigger: TriggerFunction<Data, Error, Key, ExtraArg>;
/** Function to reset the mutation state */
reset: () => void;
/** True when the mutation is in progress */
isMutating: boolean;
}
// Trigger function types based on ExtraArg requirements
type TriggerFunction<Data, Error, Key, ExtraArg> =
ExtraArg extends never
? () => Promise<Data | undefined>
: ExtraArg extends undefined
? (arg?: ExtraArg, options?: SWRMutationConfiguration<Data, Error, Key, ExtraArg>) => Promise<Data | undefined>
: (arg: ExtraArg, options?: SWRMutationConfiguration<Data, Error, Key, ExtraArg>) => Promise<Data | undefined>;Function that performs the actual mutation operation.
type MutationFetcher<Data, SWRKey, ExtraArg> = (
key: SWRKey,
options: { arg: ExtraArg }
) => Data | Promise<Data>;Mutation Fetcher Examples:
// Simple POST request
const createUser = async (url: string, { arg }: { arg: UserData }) => {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(arg)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
};
// PUT request with authentication
const updateUser = async (url: string, { arg }: { arg: Partial<User> }) => {
const response = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${getToken()}`
},
body: JSON.stringify(arg)
});
return response.json();
};
// DELETE request
const deleteUser = async (url: string) => {
await fetch(url, { method: "DELETE" });
return { deleted: true };
};
// File upload
const uploadFile = async (url: string, { arg }: { arg: File }) => {
const formData = new FormData();
formData.append("file", arg);
const response = await fetch(url, {
method: "POST",
body: formData
});
return response.json();
};
// GraphQL mutation
const graphqlMutation = async (url: string, { arg }: { arg: { query: string, variables: any } }) => {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(arg)
});
const result = await response.json();
if (result.errors) {
throw new Error(result.errors[0].message);
}
return result.data;
};Configuration options for customizing mutation behavior.
interface SWRMutationConfiguration<Data = any, Error = any, SWRMutationKey = any, ExtraArg = any> {
/** Whether to revalidate related SWR data after mutation (default: true) */
revalidate?: boolean;
/** Whether to update cache with mutation result (default: true) */
populateCache?: boolean | ((result: Data, currentData: Data | undefined) => Data);
/** Data to show optimistically during mutation */
optimisticData?: Data | ((currentData: Data | undefined) => Data);
/** Whether to rollback optimistic data on error (default: true) */
rollbackOnError?: boolean | ((error: any) => boolean);
/** Mutation fetcher function */
fetcher?: MutationFetcher<Data, SWRMutationKey, ExtraArg>;
/** Success callback */
onSuccess?: (data: Data, key: SWRMutationKey, config: SWRMutationConfiguration<Data, Error, SWRMutationKey, ExtraArg>) => void;
/** Error callback */
onError?: (err: Error, key: SWRMutationKey, config: SWRMutationConfiguration<Data, Error, SWRMutationKey, ExtraArg>) => void;
}Configuration Examples:
// Optimistic updates with rollback
const { trigger } = useSWRMutation("/api/like", likeFetcher, {
optimisticData: (current) => ({ ...current, liked: true, likes: current.likes + 1 }),
rollbackOnError: true,
populateCache: false, // Don't update cache, let revalidation handle it
revalidate: true
});
// Custom cache population
const { trigger } = useSWRMutation("/api/user", updateUser, {
populateCache: (result, currentData) => ({
...currentData,
...result,
lastUpdated: Date.now()
}),
revalidate: false // Skip revalidation since we manually populated cache
});
// Conditional rollback
const { trigger } = useSWRMutation("/api/data", mutationFetcher, {
rollbackOnError: (error) => error.status >= 500, // Only rollback on server errors
onError: (error, key) => {
if (error.status === 400) {
showValidationErrors(error.validation);
} else {
showGenericError();
}
}
});
// Success handling
const { trigger } = useSWRMutation("/api/user", createUser, {
onSuccess: (data, key) => {
showNotification(`User ${data.name} created successfully!`);
// Invalidate related data
mutate("/api/users"); // Refresh users list
}
});Common patterns for complex mutation scenarios.
Form Submission:
function UserForm() {
const [formData, setFormData] = useState({ name: "", email: "" });
const { trigger, isMutating, error } = useSWRMutation(
"/api/users",
async (url, { arg }: { arg: typeof formData }) => {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(arg)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message);
}
return response.json();
},
{
onSuccess: () => {
setFormData({ name: "", email: "" }); // Reset form
showNotification("User created successfully!");
}
}
);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await trigger(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
disabled={isMutating}
/>
<input
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
disabled={isMutating}
/>
<button type="submit" disabled={isMutating}>
{isMutating ? "Creating..." : "Create User"}
</button>
{error && <div>Error: {error.message}</div>}
</form>
);
}Optimistic Updates:
function LikeButton({ postId, initialLikes, initialLiked }: LikeButtonProps) {
const { data: post } = useSWR(`/api/posts/${postId}`, fetcher, {
fallbackData: { likes: initialLikes, liked: initialLiked }
});
const { trigger } = useSWRMutation(
`/api/posts/${postId}/like`,
async (url) => {
const response = await fetch(url, { method: "POST" });
return response.json();
},
{
optimisticData: (current) => ({
...current,
liked: !current.liked,
likes: current.liked ? current.likes - 1 : current.likes + 1
}),
rollbackOnError: true,
revalidate: false // Rely on optimistic update
}
);
const handleLike = () => trigger();
return (
<button onClick={handleLike}>
{post.liked ? "❤️" : "🤍"} {post.likes}
</button>
);
}Batch Operations:
function BulkActions({ selectedItems }: { selectedItems: string[] }) {
const { trigger: bulkDelete, isMutating } = useSWRMutation(
"/api/items/bulk-delete",
async (url, { arg }: { arg: string[] }) => {
const response = await fetch(url, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ids: arg })
});
return response.json();
},
{
onSuccess: (result) => {
showNotification(`${result.deletedCount} items deleted`);
// Revalidate lists
mutate(key => typeof key === "string" && key.startsWith("/api/items"));
}
}
);
const handleBulkDelete = async () => {
if (confirm(`Delete ${selectedItems.length} items?`)) {
await trigger(selectedItems);
}
};
return (
<button
onClick={handleBulkDelete}
disabled={isMutating || selectedItems.length === 0}
>
{isMutating ? "Deleting..." : `Delete ${selectedItems.length} items`}
</button>
);
}File Upload with Progress:
function FileUpload() {
const [uploadProgress, setUploadProgress] = useState(0);
const { trigger, isMutating, data, error } = useSWRMutation(
"/api/upload",
async (url, { arg }: { arg: File }) => {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append("file", arg);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
setUploadProgress(Math.round((e.loaded / e.total) * 100));
}
});
xhr.onload = () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`Upload failed: ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error("Upload failed"));
xhr.open("POST", url);
xhr.send(formData);
});
},
{
onSuccess: () => {
setUploadProgress(0);
showNotification("File uploaded successfully!");
}
}
);
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
trigger(file);
}
};
return (
<div>
<input
type="file"
onChange={handleFileSelect}
disabled={isMutating}
/>
{isMutating && (
<div>
<div>Uploading... {uploadProgress}%</div>
<progress value={uploadProgress} max={100} />
</div>
)}
{data && <div>Uploaded: {data.filename}</div>}
{error && <div>Error: {error.message}</div>}
</div>
);
}Install with Tessl CLI
npx tessl i tessl/npm-swr