Skip to content

Commit 97ee733

Browse files
authored
docs: typescript example (#1400)
* (docs): add typescript example for optimistic updates * (docs): add a Counter to show off the "select" option in TypeScript * (docs): simplify TypeScript example There is no need to add generics to userQuery now - it will be inferred correctly by the options * return context from onMutate and define types on the `onMutate` function * add missing select option to useQuery api reference
1 parent 3c1d2f0 commit 97ee733

File tree

9 files changed

+302
-0
lines changed

9 files changed

+302
-0
lines changed

docs/src/pages/reference/useQuery.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const {
3939
refetchOnWindowFocus,
4040
retry,
4141
retryDelay,
42+
select
4243
staleTime,
4344
structuralSharing,
4445
suspense,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
yarn.lock
15+
package-lock.json
16+
17+
# misc
18+
.DS_Store
19+
.env.local
20+
.env.development.local
21+
.env.test.local
22+
.env.production.local
23+
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install` or `yarn`
6+
- `npm run dev` or `yarn dev`
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/types/global" />
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const Module = require('module')
2+
const path = require('path')
3+
const resolveFrom = require('resolve-from')
4+
5+
const node_modules = path.resolve(__dirname, 'node_modules')
6+
7+
const originalRequire = Module.prototype.require
8+
9+
// The following ensures that there is always only a single (and same)
10+
// copy of React in an app at any given moment.
11+
Module.prototype.require = function (modulePath) {
12+
// Only redirect resolutions to non-relative and non-absolute modules
13+
if (
14+
['/react/', '/react-dom/', '/react-query/'].some(d => {
15+
try {
16+
return require.resolve(modulePath).includes(d)
17+
} catch (err) {
18+
return false
19+
}
20+
})
21+
) {
22+
try {
23+
modulePath = resolveFrom(node_modules, modulePath)
24+
} catch (err) {
25+
//
26+
}
27+
}
28+
29+
return originalRequire.call(this, modulePath)
30+
}
31+
32+
module.exports = {
33+
webpack: config => {
34+
config.resolve = {
35+
...config.resolve,
36+
alias: {
37+
...config.resolve.alias,
38+
react$: resolveFrom(path.resolve('node_modules'), 'react'),
39+
'react-query$': resolveFrom(
40+
path.resolve('node_modules'),
41+
'react-query'
42+
),
43+
'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'),
44+
},
45+
}
46+
return config
47+
},
48+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "basic",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"dependencies": {
7+
"axios": "^0.19.2",
8+
"isomorphic-unfetch": "3.0.0",
9+
"next": "9.2.2",
10+
"react": "^17.0.1",
11+
"react-dom": "^17.0.1",
12+
"react-query": "^3.2.0-beta.32",
13+
"react-query-devtools": "^3.0.0-beta.1",
14+
"typescript": "^4.1.2"
15+
},
16+
"scripts": {
17+
"dev": "next",
18+
"start": "next start",
19+
"build": "next build"
20+
}
21+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const items = []
2+
3+
export default async (req, res) => {
4+
await new Promise(r => setTimeout(r, 1000))
5+
6+
if (req.method === 'POST') {
7+
const { text } = req.body
8+
9+
// sometimes it will fail, this will cause a regression on the UI
10+
11+
if (Math.random() > 0.7) {
12+
res.status(500)
13+
res.json({ message: 'Could not add item!' })
14+
return
15+
}
16+
17+
const newTodo = { id: Math.random().toString(), text: text.toUpperCase() }
18+
items.push(newTodo)
19+
res.json(newTodo)
20+
return
21+
} else {
22+
res.json({
23+
ts: Date.now(),
24+
items,
25+
})
26+
}
27+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import * as React from 'react'
2+
import axios, { AxiosError } from 'axios'
3+
4+
import {
5+
useQuery,
6+
useQueryClient,
7+
useMutation,
8+
QueryClient,
9+
QueryClientProvider,
10+
UseQueryOptions,
11+
} from 'react-query'
12+
import { ReactQueryDevtools } from 'react-query-devtools'
13+
14+
const client = new QueryClient()
15+
16+
export default function App() {
17+
return (
18+
<QueryClientProvider client={client}>
19+
<Example />
20+
<TodoCounter />
21+
<ReactQueryDevtools initialIsOpen />
22+
</QueryClientProvider>
23+
)
24+
}
25+
26+
type Todos = {
27+
items: readonly {
28+
id: string
29+
text: string
30+
}[]
31+
ts: number
32+
}
33+
34+
async function fetchTodos(): Promise<Todos> {
35+
const res = await axios.get('/api/data')
36+
return res.data
37+
}
38+
39+
function useTodos<TData = Todos>(
40+
options?: UseQueryOptions<TData, AxiosError, Todos>
41+
) {
42+
return useQuery('todos', fetchTodos, options)
43+
}
44+
45+
function TodoCounter() {
46+
// subscribe only to changes in the 'data' prop, which will be the
47+
// amount of todos because of the select function
48+
const counterQuery = useTodos({
49+
select: data => data.items.length,
50+
notifyOnChangeProps: ['data'],
51+
})
52+
53+
React.useEffect(() => {
54+
console.log('rendering counter')
55+
})
56+
57+
return <div>TodoCounter: {counterQuery.data ?? 0}</div>
58+
}
59+
60+
function Example() {
61+
const queryClient = useQueryClient()
62+
const [text, setText] = React.useState('')
63+
const { isFetching, ...queryInfo } = useTodos()
64+
65+
const addTodoMutation = useMutation(
66+
newTodo => axios.post('/api/data', { text: newTodo }),
67+
{
68+
// When mutate is called:
69+
onMutate: async (newTodo: string) => {
70+
setText('')
71+
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
72+
await queryClient.cancelQueries('todos')
73+
74+
// Snapshot the previous value
75+
const previousTodos = queryClient.getQueryData<Todos>('todos')
76+
77+
// Optimistically update to the new value
78+
if (previousTodos) {
79+
queryClient.setQueryData<Todos>('todos', {
80+
...previousTodos,
81+
items: [
82+
...previousTodos.items,
83+
{ id: Math.random().toString(), text: newTodo },
84+
],
85+
})
86+
}
87+
88+
return { previousTodos }
89+
},
90+
// If the mutation fails, use the context returned from onMutate to roll back
91+
onError: (err, variables, context) => {
92+
if (context?.previousTodos) {
93+
queryClient.setQueryData<Todos>('todos', context.previousTodos)
94+
}
95+
},
96+
// Always refetch after error or success:
97+
onSettled: () => {
98+
queryClient.invalidateQueries('todos')
99+
},
100+
}
101+
)
102+
103+
return (
104+
<div>
105+
<p>
106+
In this example, new items can be created using a mutation. The new item
107+
will be optimistically added to the list in hopes that the server
108+
accepts the item. If it does, the list is refetched with the true items
109+
from the list. Every now and then, the mutation may fail though. When
110+
that happens, the previous list of items is restored and the list is
111+
again refetched from the server.
112+
</p>
113+
<form
114+
onSubmit={e => {
115+
e.preventDefault()
116+
addTodoMutation.mutate(text)
117+
}}
118+
>
119+
<input
120+
type="text"
121+
onChange={event => setText(event.target.value)}
122+
value={text}
123+
/>
124+
<button disabled={addTodoMutation.isLoading}>Create</button>
125+
</form>
126+
<br />
127+
{queryInfo.isSuccess && (
128+
<>
129+
<div>
130+
{/* The type of queryInfo.data will be narrowed because we check for isSuccess first */}
131+
Updated At: {new Date(queryInfo.data.ts).toLocaleTimeString()}
132+
</div>
133+
<ul>
134+
{queryInfo.data.items.map(todo => (
135+
<li key={todo.id}>{todo.text}</li>
136+
))}
137+
</ul>
138+
{isFetching && <div>Updating in background...</div>}
139+
</>
140+
)}
141+
{queryInfo.isLoading && 'Loading'}
142+
{queryInfo.error?.message}
143+
</div>
144+
)
145+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"include": [
3+
"./pages/**/*"
4+
],
5+
"compilerOptions": {
6+
"strict": true,
7+
"esModuleInterop": true,
8+
"lib": [
9+
"dom",
10+
"es2015"
11+
],
12+
"jsx": "preserve",
13+
"target": "es5",
14+
"allowJs": true,
15+
"skipLibCheck": true,
16+
"forceConsistentCasingInFileNames": true,
17+
"noEmit": true,
18+
"module": "esnext",
19+
"moduleResolution": "node",
20+
"resolveJsonModule": true,
21+
"isolatedModules": true
22+
},
23+
"exclude": [
24+
"node_modules"
25+
]
26+
}

0 commit comments

Comments
 (0)