Repository: tannerlinsley/react-query
Pull request #1399 (2020-12-14 08:05:03)

typo Walkthough -> Walkthrough

vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/tannerlinsley/react-query/78mqhwzge](https://vercel.com/tannerlinsley/react-query/78mqhwzge)  
✅ Preview: *In Progress*



Patch:
diff --git a/docs/src/pages/guides/migrating-to-react-query-3.md b/docs/src/pages/guides/migrating-to-react-query-3.md
index 6861d8efe..81e5116d6 100644
--- a/docs/src/pages/guides/migrating-to-react-query-3.md
+++ b/docs/src/pages/guides/migrating-to-react-query-3.md
@@ -97,7 +97,7 @@ try {
 }
 ```
 
-### `ReactQueryErrorResetBoundary`/`QueryCache.resetErrorBoundaries()` have been replaced by `QueryErrorResetBoundary`/`useQueryErrorResetBoundary()`.
+### `ReactQueryErrorResetBoundary` and `QueryCache.resetErrorBoundaries()` have been replaced by `QueryErrorResetBoundary` and `useQueryErrorResetBoundary()`.
 
 Together, these provide the same experience as before, but with added control to choose which component trees you want to reset. For more information, see:
 
diff --git a/docs/src/pages/overview.md b/docs/src/pages/overview.md
index dce2d4ff7..ebf783df6 100644
--- a/docs/src/pages/overview.md
+++ b/docs/src/pages/overview.md
@@ -85,4 +85,4 @@ function Example() {
 ## You talked me into it, so what now?
 
 - Consider taking the [React Query Essentials Course](https://learn.tanstack.com) (or buying it for your whole team!)
-- Learn React Query at your own pace with our amazingly thorough [Walkthough Guide](../installation) and [API Reference](../reference/useQuery)
+- Learn React Query at your own pace with our amazingly thorough [Walkthrough Guide](../installation) and [API Reference](../reference/useQuery)
diff --git a/docs/src/pages/reference/useQuery.md b/docs/src/pages/reference/useQuery.md
index 444679aee..2a1956dc8 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 000000000..613b2de63
--- /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 000000000..3ac3f1a9b
--- /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 000000000..7b7aa2c77
--- /dev/null
+++ b/examples/optimistic-updates-typescript/next-env.d.ts
@@ -0,0 +1,2 @@
+/// <reference types="next" />
+/// <reference types="next/types/global" />
diff --git a/examples/optimistic-updates-typescript/next.config.js b/examples/optimistic-updates-typescript/next.config.js
new file mode 100644
index 000000000..d38331b88
--- /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 000000000..7f678a18c
--- /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 000000000..10cab5628
--- /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 000000000..0b8569d1b
--- /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 (
+    <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>
+  )
+}
diff --git a/examples/optimistic-updates-typescript/tsconfig.json b/examples/optimistic-updates-typescript/tsconfig.json
new file mode 100644
index 000000000..108761812
--- /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"
+  ]
+}
diff --git a/src/core/utils.ts b/src/core/utils.ts
index dffaf2655..3eb8eab3c 100644
--- a/src/core/utils.ts
+++ b/src/core/utils.ts
@@ -241,7 +241,7 @@ export function partialDeepEqual(a: any, b: any): boolean {
     return false
   }
 
-  if (typeof a === 'object' && typeof b === 'object') {
+  if (a && b && typeof a === 'object' && typeof b === 'object') {
     return !Object.keys(b).some(key => !partialDeepEqual(a[key], b[key]))
   }
 


Test patch:
diff --git a/src/core/tests/utils.test.tsx b/src/core/tests/utils.test.tsx
index cac97f261..9f1f5411b 100644
--- a/src/core/tests/utils.test.tsx
+++ b/src/core/tests/utils.test.tsx
@@ -68,6 +68,30 @@ describe('core/utils', () => {
       const b = [1, 2]
       expect(partialDeepEqual(a, b)).toEqual(true)
     })
+
+    it('should return `false` if a is null and b is not', () => {
+      const a = null
+      const b = { a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }
+      expect(partialDeepEqual(a, b)).toEqual(false)
+    })
+
+    it('should return `false` if a contains null and b is not', () => {
+      const a = { a: null, c: 'c', d: [] }
+      const b = { a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }
+      expect(partialDeepEqual(a, b)).toEqual(false)
+    })
+
+    it('should return `false` if b is null and a is not', () => {
+      const a = { a: { b: 'b' }, c: 'c', d: [] }
+      const b = null
+      expect(partialDeepEqual(a, b)).toEqual(false)
+    })
+
+    it('should return `false` if b contains null and a is not', () => {
+      const a = { a: { b: 'b' }, c: 'c', d: [] }
+      const b = { a: null, c: 'c', d: [{ d: 'd ' }] }
+      expect(partialDeepEqual(a, b)).toEqual(false)
+    })
   })
 
   describe('replaceEqualDeep', () => {