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

feat: Support populateCache as a function #1818

Merged
merged 8 commits into from
Feb 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading