diff --git a/docs/src/pages/reference/useQuery.md b/docs/src/pages/reference/useQuery.md index 444679aee0..2a1956dc85 100644 --- a/docs/src/pages/reference/useQuery.md +++ b/docs/src/pages/reference/useQuery.md @@ -39,6 +39,7 @@ const { refetchOnWindowFocus, retry, retryDelay, + select staleTime, structuralSharing, suspense, diff --git a/examples/optimistic-updates-typescript/.gitignore b/examples/optimistic-updates-typescript/.gitignore new file mode 100644 index 0000000000..613b2de638 --- /dev/null +++ b/examples/optimistic-updates-typescript/.gitignore @@ -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* diff --git a/examples/optimistic-updates-typescript/README.md b/examples/optimistic-updates-typescript/README.md new file mode 100644 index 0000000000..3ac3f1a9b4 --- /dev/null +++ b/examples/optimistic-updates-typescript/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `yarn` +- `npm run dev` or `yarn dev` diff --git a/examples/optimistic-updates-typescript/next-env.d.ts b/examples/optimistic-updates-typescript/next-env.d.ts new file mode 100644 index 0000000000..7b7aa2c772 --- /dev/null +++ b/examples/optimistic-updates-typescript/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/optimistic-updates-typescript/next.config.js b/examples/optimistic-updates-typescript/next.config.js new file mode 100644 index 0000000000..d38331b88e --- /dev/null +++ b/examples/optimistic-updates-typescript/next.config.js @@ -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 + }, +} diff --git a/examples/optimistic-updates-typescript/package.json b/examples/optimistic-updates-typescript/package.json new file mode 100755 index 0000000000..7f678a18c2 --- /dev/null +++ b/examples/optimistic-updates-typescript/package.json @@ -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" + } +} diff --git a/examples/optimistic-updates-typescript/pages/api/data.js b/examples/optimistic-updates-typescript/pages/api/data.js new file mode 100755 index 0000000000..10cab56281 --- /dev/null +++ b/examples/optimistic-updates-typescript/pages/api/data.js @@ -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, + }) + } +} diff --git a/examples/optimistic-updates-typescript/pages/index.tsx b/examples/optimistic-updates-typescript/pages/index.tsx new file mode 100755 index 0000000000..0b8569d1b0 --- /dev/null +++ b/examples/optimistic-updates-typescript/pages/index.tsx @@ -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 ( + + + + + + ) +} + +type Todos = { + items: readonly { + id: string + text: string + }[] + ts: number +} + +async function fetchTodos(): Promise { + const res = await axios.get('/api/data') + return res.data +} + +function useTodos( + options?: UseQueryOptions +) { + 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
TodoCounter: {counterQuery.data ?? 0}
+} + +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') + + // Optimistically update to the new value + if (previousTodos) { + queryClient.setQueryData('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', context.previousTodos) + } + }, + // Always refetch after error or success: + onSettled: () => { + queryClient.invalidateQueries('todos') + }, + } + ) + + return ( +
+

+ 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. +

+
{ + e.preventDefault() + addTodoMutation.mutate(text) + }} + > + setText(event.target.value)} + value={text} + /> + +
+
+ {queryInfo.isSuccess && ( + <> +
+ {/* The type of queryInfo.data will be narrowed because we check for isSuccess first */} + Updated At: {new Date(queryInfo.data.ts).toLocaleTimeString()} +
+
    + {queryInfo.data.items.map(todo => ( +
  • {todo.text}
  • + ))} +
+ {isFetching &&
Updating in background...
} + + )} + {queryInfo.isLoading && 'Loading'} + {queryInfo.error?.message} +
+ ) +} diff --git a/examples/optimistic-updates-typescript/tsconfig.json b/examples/optimistic-updates-typescript/tsconfig.json new file mode 100644 index 0000000000..1087618126 --- /dev/null +++ b/examples/optimistic-updates-typescript/tsconfig.json @@ -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" + ] +}