Skip to content

Commit 425de11

Browse files
shudingkoba04
authored andcommitted
feat: Support populateCache as a function (vercel#1818)
* allow populateCache as function * test: add a test for the behavior of revalidateOnMount when the key has been changed (vercel#1847) * add tests * pass current data * update example and deps * remove next.lock Co-authored-by: Toru Kobayashi <[email protected]>
1 parent c46ef52 commit 425de11

File tree

13 files changed

+443
-148
lines changed

13 files changed

+443
-148
lines changed

examples/optimistic-ui/libs/fetch.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export default async function fetcher(...args) {
22
const res = await fetch(...args)
3+
if (!res.ok) throw new Error('Failed to fetch')
34
return res.json()
45
}

examples/optimistic-ui/pages/_app.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import '../styles.css'
2+
3+
export default function App({ Component, pageProps }) {
4+
return <Component {...pageProps} />
5+
}

examples/optimistic-ui/pages/api/data.js

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
let todos = []
2+
const delay = () => new Promise(res => setTimeout(() => res(), 1000))
3+
4+
async function getTodos() {
5+
await delay()
6+
return todos.sort((a, b) => (a.text < b.text ? -1 : 1))
7+
}
8+
9+
async function addTodo(todo) {
10+
await delay()
11+
// Sometimes it will fail, this will cause a regression on the UI
12+
if (Math.random() < 0.2 || !todo.text)
13+
throw new Error('Failed to add new item!')
14+
todo.text = todo.text.charAt(0).toUpperCase() + todo.text.slice(1)
15+
todos = [...todos, todo]
16+
return todo
17+
}
18+
19+
export default async function api(req, res) {
20+
try {
21+
if (req.method === 'POST') {
22+
const body = JSON.parse(req.body)
23+
return res.json(await addTodo(body))
24+
}
25+
26+
return res.json(await getTodos())
27+
} catch (err) {
28+
return res.status(500).json({ error: err.message })
29+
}
30+
}

examples/optimistic-ui/pages/index.js

Lines changed: 107 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,110 @@
1-
import React from 'react'
1+
import useSWR from 'swr'
2+
import React, { useState } from 'react'
3+
24
import fetch from '../libs/fetch'
35

4-
import useSWR, { mutate } from 'swr'
5-
6-
export default function Index() {
7-
const [text, setText] = React.useState('');
8-
const { data } = useSWR('/api/data', fetch)
9-
10-
async function handleSubmit(event) {
11-
event.preventDefault()
12-
// Call mutate to optimistically update the UI.
13-
mutate('/api/data', [...data, text], false)
14-
// Then we send the request to the API and let mutate
15-
// update the data with the API response.
16-
// Our action may fail in the API function, and the response differ
17-
// from what was optimistically updated, in that case the UI will be
18-
// changed to match the API response.
19-
// The fetch could also fail, in that case the UI will
20-
// be in an incorrect state until the next successful fetch.
21-
mutate('/api/data', await fetch('/api/data', {
22-
method: 'POST',
23-
body: JSON.stringify({ text })
24-
}))
25-
setText('')
26-
}
27-
28-
return <div>
29-
<form onSubmit={handleSubmit}>
30-
<input
31-
type="text"
32-
onChange={event => setText(event.target.value)}
33-
value={text}
34-
/>
35-
<button>Create</button>
36-
</form>
37-
<ul>
38-
{data ? data.map(datum => <li key={datum}>{datum}</li>) : 'loading...'}
39-
</ul>
40-
</div>
6+
export default function App() {
7+
const [text, setText] = useState('')
8+
const { data, mutate } = useSWR('/api/todos', fetch)
9+
10+
const [state, setState] = useState(<span className="info">&nbsp;</span>)
11+
12+
return (
13+
<div>
14+
{/* <Toaster toastOptions={{ position: 'bottom-center' }} /> */}
15+
<h1>Optimistic UI with SWR</h1>
16+
17+
<p className="note">
18+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
19+
<path d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm1 18h-2v-8h2v8zm-1-12.25c.69 0 1.25.56 1.25 1.25s-.56 1.25-1.25 1.25-1.25-.56-1.25-1.25.56-1.25 1.25-1.25z" />
20+
</svg>
21+
This application optimistically updates the data, while revalidating in
22+
the background. The <code>POST</code> API auto capitializes the data,
23+
and only returns the new added one instead of the full list. And the{' '}
24+
<code>GET</code> API returns the full list in order.
25+
</p>
26+
27+
<form onSubmit={ev => ev.preventDefault()}>
28+
<input
29+
value={text}
30+
onChange={e => setText(e.target.value)}
31+
placeholder="Add your to-do here..."
32+
autoFocus
33+
/>
34+
<button
35+
type="submit"
36+
onClick={async () => {
37+
setText('')
38+
setState(
39+
<span className="info">Showing optimistic data, mutating...</span>
40+
)
41+
42+
const newTodo = {
43+
id: Date.now(),
44+
text
45+
}
46+
47+
try {
48+
// Update the local state immediately and fire the
49+
// request. Since the API will return the updated
50+
// data, there is no need to start a new revalidation
51+
// and we can directly populate the cache.
52+
await mutate(
53+
fetch('/api/todos', {
54+
method: 'POST',
55+
body: JSON.stringify(newTodo)
56+
}),
57+
{
58+
optimisticData: [...data, newTodo],
59+
rollbackOnError: true,
60+
populateCache: newItem => {
61+
setState(
62+
<span className="success">
63+
Succesfully mutated the resource and populated cache.
64+
Revalidating...
65+
</span>
66+
)
67+
68+
return [...data, newItem]
69+
},
70+
revalidate: true
71+
}
72+
)
73+
setState(<span className="info">Revalidated the resource.</span>)
74+
} catch (e) {
75+
// If the API errors, the original data will be
76+
// rolled back by SWR automatically.
77+
setState(
78+
<span className="error">
79+
Failed to mutate. Rolled back to previous state and
80+
revalidated the resource.
81+
</span>
82+
)
83+
}
84+
}}
85+
>
86+
Add
87+
</button>
88+
</form>
89+
90+
{state}
91+
92+
<ul>
93+
{data ? (
94+
data.length ? (
95+
data.map(todo => {
96+
return <li key={todo.id}>{todo.text}</li>
97+
})
98+
) : (
99+
<i>
100+
No todos yet. Try adding lowercased "banana" and "apple" to the
101+
list.
102+
</i>
103+
)
104+
) : (
105+
<i>Loading...</i>
106+
)}
107+
</ul>
108+
</div>
109+
)
41110
}

examples/optimistic-ui/styles.css

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
html {
2+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
3+
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
4+
text-align: center;
5+
}
6+
7+
body {
8+
max-width: 600px;
9+
margin: auto;
10+
}
11+
12+
h1 {
13+
margin-top: 1em;
14+
}
15+
16+
.note {
17+
text-align: left;
18+
font-size: 0.9em;
19+
line-height: 1.5;
20+
color: #666;
21+
}
22+
23+
.note svg {
24+
margin-right: 0.5em;
25+
vertical-align: -2px;
26+
width: 14px;
27+
height: 14px;
28+
margin-right: 5px;
29+
}
30+
31+
form {
32+
display: flex;
33+
margin: 8px 0;
34+
gap: 8px;
35+
}
36+
37+
input {
38+
flex: 1;
39+
}
40+
41+
input,
42+
button {
43+
font-size: 16px;
44+
padding: 6px 5px;
45+
}
46+
47+
code {
48+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
49+
Liberation Mono, Courier New, monospace;
50+
font-feature-settings: 'rlig' 1, 'calt' 1, 'ss01' 1;
51+
background-color: #eee;
52+
padding: 1px 3px;
53+
border-radius: 2px;
54+
}
55+
56+
ul {
57+
text-align: left;
58+
list-style: none;
59+
padding: 0;
60+
}
61+
62+
li {
63+
margin: 8px 0;
64+
padding: 10px;
65+
border-radius: 4px;
66+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12), 0 0 0 1px #ededed;
67+
}
68+
69+
i {
70+
color: #999;
71+
}
72+
73+
.info,
74+
.success,
75+
.error {
76+
display: block;
77+
text-align: left;
78+
padding: 6px 0;
79+
font-size: 0.9em;
80+
opacity: 0.9;
81+
}
82+
83+
.info {
84+
color: #666;
85+
}
86+
.success {
87+
color: #4caf50;
88+
}
89+
.error {
90+
color: #f44336;
91+
}

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
"husky": "2.4.1",
9898
"jest": "27.0.6",
9999
"lint-staged": "8.2.1",
100-
"next": "12.0.9",
100+
"next": "^12.1.0",
101101
"npm-run-all": "4.1.5",
102102
"prettier": "2.5.0",
103103
"react": "17.0.1",
@@ -112,11 +112,11 @@
112112
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
113113
},
114114
"prettier": {
115+
"tabWidth": 2,
115116
"semi": false,
116-
"singleQuote": true,
117117
"useTabs": false,
118-
"trailingComma": "none",
119-
"tabWidth": 2,
120-
"arrowParens": "avoid"
118+
"singleQuote": true,
119+
"arrowParens": "avoid",
120+
"trailingComma": "none"
121121
}
122122
}

src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,12 @@ export type Arguments =
139139
export type Key = Arguments | (() => Arguments)
140140

141141
export type MutatorCallback<Data = any> = (
142-
currentValue?: Data
142+
currentData?: Data
143143
) => Promise<undefined | Data> | undefined | Data
144144

145145
export type MutatorOptions<Data = any> = {
146146
revalidate?: boolean
147-
populateCache?: boolean
147+
populateCache?: boolean | ((result: any, currentData: Data) => Data)
148148
optimisticData?: Data
149149
rollbackOnError?: boolean
150150
}

src/use-swr.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,9 @@ export const useSWRHandler = <Data = any, Error = any>(
360360
// eslint-disable-next-line react-hooks/exhaustive-deps
361361
const boundMutate: SWRResponse<Data, Error>['mutate'] = useCallback(
362362
// By using `bind` we don't need to modify the size of the rest arguments.
363-
internalMutate.bind(UNDEFINED, cache, () => keyRef.current),
363+
// Due to https://github.com/microsoft/TypeScript/issues/37181, we have to
364+
// cast it to any for now.
365+
internalMutate.bind(UNDEFINED, cache, () => keyRef.current) as any,
364366
// eslint-disable-next-line react-hooks/exhaustive-deps
365367
[]
366368
)

src/utils/helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export const noop = () => {}
44
// by something else. Prettier ignore and extra parentheses are necessary here
55
// to ensure that tsc doesn't remove the __NOINLINE__ comment.
66
// prettier-ignore
7-
export const UNDEFINED: undefined = (/*#__NOINLINE__*/ noop()) as undefined
7+
export const UNDEFINED = (/*#__NOINLINE__*/ noop()) as undefined
88

99
export const OBJECT = Object
1010

0 commit comments

Comments
 (0)