Skip to content

Commit

Permalink
docs: typescript example (#1400)
Browse files Browse the repository at this point in the history
* (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
  • Loading branch information
TkDodo authored Dec 14, 2020
1 parent 3c1d2f0 commit 97ee733
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/src/pages/reference/useQuery.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const {
refetchOnWindowFocus,
retry,
retryDelay,
select
staleTime,
structuralSharing,
suspense,
Expand Down
26 changes: 26 additions & 0 deletions examples/optimistic-updates-typescript/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

yarn.lock
package-lock.json

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
6 changes: 6 additions & 0 deletions examples/optimistic-updates-typescript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example

To run this example:

- `npm install` or `yarn`
- `npm run dev` or `yarn dev`
2 changes: 2 additions & 0 deletions examples/optimistic-updates-typescript/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
48 changes: 48 additions & 0 deletions examples/optimistic-updates-typescript/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const Module = require('module')
const path = require('path')
const resolveFrom = require('resolve-from')

const node_modules = path.resolve(__dirname, 'node_modules')

const originalRequire = Module.prototype.require

// The following ensures that there is always only a single (and same)
// copy of React in an app at any given moment.
Module.prototype.require = function (modulePath) {
// Only redirect resolutions to non-relative and non-absolute modules
if (
['/react/', '/react-dom/', '/react-query/'].some(d => {
try {
return require.resolve(modulePath).includes(d)
} catch (err) {
return false
}
})
) {
try {
modulePath = resolveFrom(node_modules, modulePath)
} catch (err) {
//
}
}

return originalRequire.call(this, modulePath)
}

module.exports = {
webpack: config => {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
react$: resolveFrom(path.resolve('node_modules'), 'react'),
'react-query$': resolveFrom(
path.resolve('node_modules'),
'react-query'
),
'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'),
},
}
return config
},
}
21 changes: 21 additions & 0 deletions examples/optimistic-updates-typescript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "basic",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"axios": "^0.19.2",
"isomorphic-unfetch": "3.0.0",
"next": "9.2.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-query": "^3.2.0-beta.32",
"react-query-devtools": "^3.0.0-beta.1",
"typescript": "^4.1.2"
},
"scripts": {
"dev": "next",
"start": "next start",
"build": "next build"
}
}
27 changes: 27 additions & 0 deletions examples/optimistic-updates-typescript/pages/api/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const items = []

export default async (req, res) => {
await new Promise(r => setTimeout(r, 1000))

if (req.method === 'POST') {
const { text } = req.body

// sometimes it will fail, this will cause a regression on the UI

if (Math.random() > 0.7) {
res.status(500)
res.json({ message: 'Could not add item!' })
return
}

const newTodo = { id: Math.random().toString(), text: text.toUpperCase() }
items.push(newTodo)
res.json(newTodo)
return
} else {
res.json({
ts: Date.now(),
items,
})
}
}
145 changes: 145 additions & 0 deletions examples/optimistic-updates-typescript/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import * as React from 'react'
import axios, { AxiosError } from 'axios'

import {
useQuery,
useQueryClient,
useMutation,
QueryClient,
QueryClientProvider,
UseQueryOptions,
} from 'react-query'
import { ReactQueryDevtools } from 'react-query-devtools'

const client = new QueryClient()

export default function App() {
return (
<QueryClientProvider client={client}>
<Example />
<TodoCounter />
<ReactQueryDevtools initialIsOpen />
</QueryClientProvider>
)
}

type Todos = {
items: readonly {
id: string
text: string
}[]
ts: number
}

async function fetchTodos(): Promise<Todos> {
const res = await axios.get('/api/data')
return res.data
}

function useTodos<TData = Todos>(
options?: UseQueryOptions<TData, AxiosError, Todos>
) {
return useQuery('todos', fetchTodos, options)
}

function TodoCounter() {
// subscribe only to changes in the 'data' prop, which will be the
// amount of todos because of the select function
const counterQuery = useTodos({
select: data => data.items.length,
notifyOnChangeProps: ['data'],
})

React.useEffect(() => {
console.log('rendering counter')
})

return <div>TodoCounter: {counterQuery.data ?? 0}</div>
}

function Example() {
const queryClient = useQueryClient()
const [text, setText] = React.useState('')
const { isFetching, ...queryInfo } = useTodos()

const addTodoMutation = useMutation(
newTodo => axios.post('/api/data', { text: newTodo }),
{
// When mutate is called:
onMutate: async (newTodo: string) => {
setText('')
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries('todos')

// Snapshot the previous value
const previousTodos = queryClient.getQueryData<Todos>('todos')

// Optimistically update to the new value
if (previousTodos) {
queryClient.setQueryData<Todos>('todos', {
...previousTodos,
items: [
...previousTodos.items,
{ id: Math.random().toString(), text: newTodo },
],
})
}

return { previousTodos }
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (err, variables, context) => {
if (context?.previousTodos) {
queryClient.setQueryData<Todos>('todos', context.previousTodos)
}
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries('todos')
},
}
)

return (
<div>
<p>
In this example, new items can be created using a mutation. The new item
will be optimistically added to the list in hopes that the server
accepts the item. If it does, the list is refetched with the true items
from the list. Every now and then, the mutation may fail though. When
that happens, the previous list of items is restored and the list is
again refetched from the server.
</p>
<form
onSubmit={e => {
e.preventDefault()
addTodoMutation.mutate(text)
}}
>
<input
type="text"
onChange={event => setText(event.target.value)}
value={text}
/>
<button disabled={addTodoMutation.isLoading}>Create</button>
</form>
<br />
{queryInfo.isSuccess && (
<>
<div>
{/* The type of queryInfo.data will be narrowed because we check for isSuccess first */}
Updated At: {new Date(queryInfo.data.ts).toLocaleTimeString()}
</div>
<ul>
{queryInfo.data.items.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
{isFetching && <div>Updating in background...</div>}
</>
)}
{queryInfo.isLoading && 'Loading'}
{queryInfo.error?.message}
</div>
)
}
26 changes: 26 additions & 0 deletions examples/optimistic-updates-typescript/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"include": [
"./pages/**/*"
],
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"lib": [
"dom",
"es2015"
],
"jsx": "preserve",
"target": "es5",
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true
},
"exclude": [
"node_modules"
]
}

1 comment on commit 97ee733

@vercel
Copy link

@vercel vercel bot commented on 97ee733 Dec 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.