Skip to content

Commit

Permalink
feat: Support populateCache as a function (#1818)
Browse files Browse the repository at this point in the history
* allow populateCache as function

* test: add a test for the behavior of revalidateOnMount when the key has been changed (#1847)

* add tests

* pass current data

* update example and deps

* remove next.lock

Co-authored-by: Toru Kobayashi <koba0004@gmail.com>
  • Loading branch information
shuding and koba04 committed Feb 18, 2022
1 parent ef400ea commit baaafc2
Show file tree
Hide file tree
Showing 13 changed files with 443 additions and 148 deletions.
1 change: 1 addition & 0 deletions examples/optimistic-ui/libs/fetch.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export default async function fetcher(...args) {
const res = await fetch(...args)
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
}
5 changes: 5 additions & 0 deletions examples/optimistic-ui/pages/_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import '../styles.css'

export default function App({ Component, pageProps }) {
return <Component {...pageProps} />
}
22 changes: 0 additions & 22 deletions examples/optimistic-ui/pages/api/data.js

This file was deleted.

30 changes: 30 additions & 0 deletions examples/optimistic-ui/pages/api/todos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
let todos = []
const delay = () => new Promise(res => setTimeout(() => res(), 1000))

async function getTodos() {
await delay()
return todos.sort((a, b) => (a.text < b.text ? -1 : 1))
}

async function addTodo(todo) {
await delay()
// Sometimes it will fail, this will cause a regression on the UI
if (Math.random() < 0.2 || !todo.text)
throw new Error('Failed to add new item!')
todo.text = todo.text.charAt(0).toUpperCase() + todo.text.slice(1)
todos = [...todos, todo]
return todo
}

export default async function api(req, res) {
try {
if (req.method === 'POST') {
const body = JSON.parse(req.body)
return res.json(await addTodo(body))
}

return res.json(await getTodos())
} catch (err) {
return res.status(500).json({ error: err.message })
}
}
145 changes: 107 additions & 38 deletions examples/optimistic-ui/pages/index.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,110 @@
import React from 'react'
import useSWR from 'swr'
import React, { useState } from 'react'

import fetch from '../libs/fetch'

import useSWR, { mutate } from 'swr'

export default function Index() {
const [text, setText] = React.useState('');
const { data } = useSWR('/api/data', fetch)

async function handleSubmit(event) {
event.preventDefault()
// Call mutate to optimistically update the UI.
mutate('/api/data', [...data, text], false)
// Then we send the request to the API and let mutate
// update the data with the API response.
// Our action may fail in the API function, and the response differ
// from what was optimistically updated, in that case the UI will be
// changed to match the API response.
// The fetch could also fail, in that case the UI will
// be in an incorrect state until the next successful fetch.
mutate('/api/data', await fetch('/api/data', {
method: 'POST',
body: JSON.stringify({ text })
}))
setText('')
}

return <div>
<form onSubmit={handleSubmit}>
<input
type="text"
onChange={event => setText(event.target.value)}
value={text}
/>
<button>Create</button>
</form>
<ul>
{data ? data.map(datum => <li key={datum}>{datum}</li>) : 'loading...'}
</ul>
</div>
export default function App() {
const [text, setText] = useState('')
const { data, mutate } = useSWR('/api/todos', fetch)

const [state, setState] = useState(<span className="info">&nbsp;</span>)

return (
<div>
{/* <Toaster toastOptions={{ position: 'bottom-center' }} /> */}
<h1>Optimistic UI with SWR</h1>

<p className="note">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<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" />
</svg>
This application optimistically updates the data, while revalidating in
the background. The <code>POST</code> API auto capitializes the data,
and only returns the new added one instead of the full list. And the{' '}
<code>GET</code> API returns the full list in order.
</p>

<form onSubmit={ev => ev.preventDefault()}>
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder="Add your to-do here..."
autoFocus
/>
<button
type="submit"
onClick={async () => {
setText('')
setState(
<span className="info">Showing optimistic data, mutating...</span>
)

const newTodo = {
id: Date.now(),
text
}

try {
// Update the local state immediately and fire the
// request. Since the API will return the updated
// data, there is no need to start a new revalidation
// and we can directly populate the cache.
await mutate(
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo)
}),
{
optimisticData: [...data, newTodo],
rollbackOnError: true,
populateCache: newItem => {
setState(
<span className="success">
Succesfully mutated the resource and populated cache.
Revalidating...
</span>
)

return [...data, newItem]
},
revalidate: true
}
)
setState(<span className="info">Revalidated the resource.</span>)
} catch (e) {
// If the API errors, the original data will be
// rolled back by SWR automatically.
setState(
<span className="error">
Failed to mutate. Rolled back to previous state and
revalidated the resource.
</span>
)
}
}}
>
Add
</button>
</form>

{state}

<ul>
{data ? (
data.length ? (
data.map(todo => {
return <li key={todo.id}>{todo.text}</li>
})
) : (
<i>
No todos yet. Try adding lowercased "banana" and "apple" to the
list.
</i>
)
) : (
<i>Loading...</i>
)}
</ul>
</div>
)
}
91 changes: 91 additions & 0 deletions examples/optimistic-ui/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
html {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
text-align: center;
}

body {
max-width: 600px;
margin: auto;
}

h1 {
margin-top: 1em;
}

.note {
text-align: left;
font-size: 0.9em;
line-height: 1.5;
color: #666;
}

.note svg {
margin-right: 0.5em;
vertical-align: -2px;
width: 14px;
height: 14px;
margin-right: 5px;
}

form {
display: flex;
margin: 8px 0;
gap: 8px;
}

input {
flex: 1;
}

input,
button {
font-size: 16px;
padding: 6px 5px;
}

code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
Liberation Mono, Courier New, monospace;
font-feature-settings: 'rlig' 1, 'calt' 1, 'ss01' 1;
background-color: #eee;
padding: 1px 3px;
border-radius: 2px;
}

ul {
text-align: left;
list-style: none;
padding: 0;
}

li {
margin: 8px 0;
padding: 10px;
border-radius: 4px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12), 0 0 0 1px #ededed;
}

i {
color: #999;
}

.info,
.success,
.error {
display: block;
text-align: left;
padding: 6px 0;
font-size: 0.9em;
opacity: 0.9;
}

.info {
color: #666;
}
.success {
color: #4caf50;
}
.error {
color: #f44336;
}
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"husky": "2.4.1",
"jest": "27.0.6",
"lint-staged": "8.2.1",
"next": "12.0.9",
"next": "^12.1.0",
"npm-run-all": "4.1.5",
"prettier": "2.5.0",
"react": "17.0.1",
Expand All @@ -112,11 +112,11 @@
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
},
"prettier": {
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"useTabs": false,
"trailingComma": "none",
"tabWidth": 2,
"arrowParens": "avoid"
"singleQuote": true,
"arrowParens": "avoid",
"trailingComma": "none"
}
}
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,12 @@ export type Arguments =
export type Key = Arguments | (() => Arguments)

export type MutatorCallback<Data = any> = (
currentValue?: Data
currentData?: Data
) => Promise<undefined | Data> | undefined | Data

export type MutatorOptions<Data = any> = {
revalidate?: boolean
populateCache?: boolean
populateCache?: boolean | ((result: any, currentData: Data) => Data)
optimisticData?: Data
rollbackOnError?: boolean
}
Expand Down
4 changes: 3 additions & 1 deletion src/use-swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,9 @@ export const useSWRHandler = <Data = any, Error = any>(
// eslint-disable-next-line react-hooks/exhaustive-deps
const boundMutate: SWRResponse<Data, Error>['mutate'] = useCallback(
// By using `bind` we don't need to modify the size of the rest arguments.
internalMutate.bind(UNDEFINED, cache, () => keyRef.current),
// Due to https://github.com/microsoft/TypeScript/issues/37181, we have to
// cast it to any for now.
internalMutate.bind(UNDEFINED, cache, () => keyRef.current) as any,
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)
Expand Down
2 changes: 1 addition & 1 deletion src/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const noop = () => {}
// by something else. Prettier ignore and extra parentheses are necessary here
// to ensure that tsc doesn't remove the __NOINLINE__ comment.
// prettier-ignore
export const UNDEFINED: undefined = (/*#__NOINLINE__*/ noop()) as undefined
export const UNDEFINED = (/*#__NOINLINE__*/ noop()) as undefined

export const OBJECT = Object

Expand Down

0 comments on commit baaafc2

Please sign in to comment.