CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-swr

React Hooks library for remote data fetching with stale-while-revalidate caching strategy

Pending
Overview
Eval results
Files

mutations.mddocs/

Remote Mutations

The useSWRMutation hook handles remote mutations (POST, PUT, DELETE, PATCH) with optimistic updates, error handling, and cache management.

Capabilities

useSWRMutation Hook

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,
  }
);

SWR Mutation Response

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

Mutation Fetcher

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

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
  }
});

Advanced Mutation Patterns

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

docs

cache-management.md

core-data-fetching.md

global-configuration.md

immutable-data.md

index.md

infinite-loading.md

mutations.md

subscriptions.md

tile.json