Repository: zeit/swr
Pull request #134 (2019-11-19 12:14:51)

Add async mutation support
With this change, the `data` parameter of `mutate` can be a promise, which returns the data or throws an error:

```js
mutate(key, Promise<Data>)
```

It can simplify scenarios like

```js
const newData = await update(...)   // some POST APIs return the updated data
mutate('/api/data', newData, false) // disable the revalidation here
```

to:

```js
mutate('/api/data', update(...)) // POST + mutate w/o revalidation
```

Related: #93.


Patch:
diff --git a/README.md b/README.md
index 8392383..ad7ea49 100644
--- a/README.md
+++ b/README.md
@@ -338,6 +338,18 @@ function Profile () {
 }
 ```
 
+Clicking the button in the example above will send a POST request to modify the remote data, locally update the client data and
+try to fetch the latest one (revalidate).
+
+But many POST APIs will just return the updated data directly, so we don’t need to revalidate again.  
+Here’s an example showing the “local mutate - request - update” usage:
+
+```js
+mutate('/api/user', newUser, false)      // use `false` to mutate without revalidation
+mutate('/api/user', updateUser(newUser)) // `updateUser` is a Promise of the request,
+                                         // which returns the updated document
+```
+
 ### SSR with Next.js
 
 With the `initialData` option, you pass an initial value to the hook. It works perfectly with many SSR solutions
diff --git a/src/types.ts b/src/types.ts
index 98c7a2d..10d73d5 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -57,7 +57,7 @@ export type triggerInterface = (
 ) => void
 export type mutateInterface<Data = any> = (
   key: keyInterface,
-  data: Data,
+  data: Data | Promise<Data>,
   shouldRevalidate?: boolean
 ) => void
 export type broadcastStateInterface<Data = any, Error = any> = (
diff --git a/src/use-swr.ts b/src/use-swr.ts
index 55f6cd2..d9cd833 100644
--- a/src/use-swr.ts
+++ b/src/use-swr.ts
@@ -92,21 +92,42 @@ const broadcastState: broadcastStateInterface = (key, data, error) => {
   }
 }
 
-const mutate: mutateInterface = (_key, data, shouldRevalidate = true) => {
+const mutate: mutateInterface = async (_key, _data, shouldRevalidate) => {
   const [key] = getKeyArgs(_key)
   if (!key) return
 
   // update timestamp
   MUTATION_TS[key] = Date.now() - 1
 
-  // update cached data
-  cacheSet(key, data)
+  let data, error
+
+  if (_data && typeof _data.then === 'function') {
+    // `_data` is a promise
+    try {
+      data = await _data
+    } catch (err) {
+      error = err
+    }
+  } else {
+    data = _data
+
+    if (typeof shouldRevalidate === 'undefined') {
+      // if it's a sync mutation, we trigger the revalidation by default
+      // because in most cases it's a local mutation
+      shouldRevalidate = true
+    }
+  }
+
+  if (typeof data !== 'undefined') {
+    // update cached data
+    cacheSet(key, data)
+  }
 
   // update existing SWR Hooks' state
   const updaters = CACHE_REVALIDATORS[key]
   if (updaters) {
     for (let i = 0; i < updaters.length; ++i) {
-      updaters[i](shouldRevalidate, data)
+      updaters[i](!!shouldRevalidate, data, error)
     }
   }
 }


Test patch:
diff --git a/test/use-swr.test.tsx b/test/use-swr.test.tsx
index ed934da..6c45d5d 100644
--- a/test/use-swr.test.tsx
+++ b/test/use-swr.test.tsx
@@ -602,6 +602,30 @@ describe('useSWR - local mutation', () => {
       `"data: truth"`
     )
   })
+
+  it('should support async mutation', async () => {
+    function Page() {
+      const { data } = useSWR('mutate-1', () => 0, {
+        dedupingInterval: 0
+      })
+      return <div>data: {data}</div>
+    }
+    const { container } = render(<Page />)
+
+    // hydration
+    expect(container.textContent).toMatchInlineSnapshot(`"data: "`)
+    await waitForDomChange({ container }) // mount
+    expect(container.textContent).toMatchInlineSnapshot(`"data: 0"`)
+    await act(() => {
+      // mutate and revalidate
+      return mutate(
+        'mutate-1',
+        new Promise(res => setTimeout(() => res(999), 100))
+      )
+    })
+    await act(() => new Promise(res => setTimeout(res, 110)))
+    expect(container.textContent).toMatchInlineSnapshot(`"data: 999"`)
+  })
 })
 
 describe('useSWR - context configs', () => {