Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bound/global mutate functions do not mutate remote data #2971

Open
TrevorBurnham opened this issue May 24, 2024 · 2 comments
Open

Bound/global mutate functions do not mutate remote data #2971

TrevorBurnham opened this issue May 24, 2024 · 2 comments

Comments

@TrevorBurnham
Copy link

Bug report

I'm a longtime SWR user, but I'm still puzzled by the behavior of the three different mutate functions:

  1. The bound mutate function returned by useSWR.
  2. The global mutate function accessible via useSWRConfig or with import { mutate } from "swr".
  3. The mutation trigger function returned by useSWRMutation.

The docs seem to describe these three functions as three ways to achieve the same goal: mutating data. When I hear "mutating data" in the context of a framework like SWR, I assume that the goal is to update data both remotely and locally, either via an optimistic update (update the local data immediately and revert it if the remote update fails) or a pessimistic update (wait to update the local data until the remote update is confirmed).

However, in practice it seems that only useSWRMutation performs remote updates. The other two mutation functions only affect local state, which creates an inconsistency with the remote state.

That means that you get some very strange behavior by default: If you use the bound or global mutate function to perform an update, with no additional parameters, then that update will be reverted moments later when revalidation occurs.

Description / Observed Behavior

Let's look at an example (a simplified version of the CodeSandbox below):

const { data, mutate } = useSWR<{ count: number }>("/count");

return (
  <div>
    <span>Count: {data?.count ?? ""}</span>
    <button onClick={(() => {
      mutate({ count: data.count   1 });
    }}>
      Increment
    </button>
  </div>
)

This looks like a straightforward use of the bound mutate function. However, if you click the button, the count will go up by 1, then back down to its original value when revalidation occurs, because the remote state is not modified.

Expected Behavior

I'd expect the bound and global mutate functions to trigger the fetcher to modify the remote state, like useSWRMutation does. The optimistic updates example in the docs seems to show the global mutate function being used in this way, but I've never been able to get it to work; is there something I'm missing?

Alternatively, I'd expect the docs to be very clear that the bound and global mutate functions are only useful when you're making the remote update separately, and that useSWRMutation is the preferred way of performing mutations.

Repro Steps / Code Example

This CodeSandbox demonstrates how the three mutation functions behave in a very simple scenario: https://codesandbox.io/p/devbox/focused-euler-sp668n?file=/app/page.tsx:20,14

Additional Context

SWR version: 2.2.5

@koba04
Copy link
Collaborator

koba04 commented May 25, 2024

When you call mutate returned from useSWR, this make a revalidation.

In the following case, the fetcher is called again.

const { data, mutate } = useSWR<{ count: number }>("/count", fetcher);

return (
  <div>
    <span>Count: {data?.count ?? ""}</span>
    <button onClick={(() => {
      mutate({ count: data.count   1 });
    }}>
      Increment
    </button>
  </div>
)

If you want to make a remote mutation, you have to call it manually.

const updateCount = (count) => {
  return fetch("/count", {
    method: "PATCH",
    body: JSON.stringify(count),
    headers: { "Content-Type": "application/json" },
  });
}

mutate(() => updateCount(newCount);

@TrevorBurnham
Copy link
Author

@koba04 Thanks for the response! I think you're right that the behavior of mutate makes sense if you pass an updater to it. My confusion is around how the docs (and to some extent the TypeScript types) present the usage of mutate.

In the bound mutate section of the docs, the only example shows an updater being called separately from the mutate call:

// send a request to the API to update the data
await requestUpdateUsername(newName)
// update the local data immediately and revalidate (refetch)
// NOTE: key is not required when using useSWR's mutate as it's pre-bound
mutate({ ...data, name: newName })

That example seems clearly suboptimal: It implies two network requests (the update followed by a revalidation) when you should only need one, the UI doesn't respond to the user's click until after the update request has completed, and there's no failure handling.

Then if you scroll down to the API parameters section, the docs describe the data parameter in mutate(data) as:

data to update the client cache, or an async function for the remote mutation

If data is the former, as the parameter name suggests, then most of the options don't make any sense: optimisticData, populateCache, rollbackOnError, and throwOnError are no-ops. Yet the docs don't explicitly say that, and there's no error or console warning if you try to use them.

IMO it would make more sense for the docs to present a mutate(updater) example first (as it later does in the Optimistic Updates section), and then secondarily note the other forms:

  • mutate() can be used to trigger revalidation.
  • mutate(data) can be used to directly update local data and trigger revalidation.
  • mutate(data, {revalidate: false}) can be used to directly update local data without triggering revalidation.

I'd be happy to contribute a draft if other folks feel the same way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants