├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .github ├── FUNDING.yml └── size-plugin.yml ├── .gitignore ├── .size-snapshot.json ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── auto-refetching │ ├── README.md │ ├── components │ │ └── button.js │ ├── libs │ │ └── fetch.js │ ├── package.json │ ├── pages │ │ ├── api │ │ │ └── data.js │ │ └── index.js │ └── yarn.lock ├── basic │ ├── README.md │ ├── libs │ │ └── fetch.js │ ├── package.json │ ├── pages │ │ ├── [user] │ │ │ └── [repo].js │ │ ├── api │ │ │ └── data.js │ │ └── index.js │ └── yarn.lock ├── custom-hooks │ ├── README.md │ ├── hooks │ │ ├── use-projects.js │ │ └── use-repository.js │ ├── libs │ │ └── fetch.js │ ├── package.json │ ├── pages │ │ ├── [user] │ │ │ └── [repo].js │ │ ├── api │ │ │ └── data.js │ │ └── index.js │ └── yarn.lock ├── focus-refetching │ ├── README.md │ ├── components │ │ └── button.js │ ├── libs │ │ ├── auth.js │ │ └── fetch.js │ ├── package.json │ ├── pages │ │ ├── api │ │ │ └── user.js │ │ └── index.js │ └── yarn.lock ├── load-more-pagination │ ├── README.md │ ├── libs │ │ └── fetch.js │ ├── package.json │ ├── pages │ │ ├── about.js │ │ ├── api │ │ │ └── projects.js │ │ └── index.js │ └── yarn.lock ├── optimistic-updates │ ├── README.md │ ├── libs │ │ └── fetch.js │ ├── package.json │ ├── pages │ │ ├── api │ │ │ └── data.js │ │ └── index.js │ └── yarn.lock ├── sandbox │ ├── .babelrc │ ├── .eslintrc │ ├── .gitignore │ ├── .prettierrc │ ├── .rescriptsrc.js │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── src │ │ ├── index.app.js │ │ ├── index.js │ │ └── styles.css │ └── yarn.lock └── suspense │ ├── .babelrc │ ├── .eslintrc │ ├── .gitignore │ ├── .prettierrc │ ├── .rescriptsrc.js │ ├── README.md │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ ├── src │ ├── components │ │ ├── Button.js │ │ ├── ErrorBounderay.js │ │ ├── Project.js │ │ ├── Projects.js │ │ └── Spinner.js │ ├── index.js │ ├── queries.js │ └── styles.css │ └── yarn.lock ├── media ├── header.png ├── logo.png └── logo.sketch ├── package.json ├── prettier.config.js ├── rollup.config.js ├── sizes-cjs.json ├── sizes-es.json ├── src ├── __tests__ │ └── setQueryData-test.js └── index.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "modules": false, 7 | "exclude": ["@babel/plugin-transform-regenerator"] 8 | } 9 | ], 10 | "@babel/react" 11 | ], 12 | "plugins": [ 13 | [ 14 | "module:fast-async", 15 | { 16 | "compiler": { 17 | "noRuntime": true 18 | } 19 | } 20 | ] 21 | ], 22 | "env": { 23 | "test": { 24 | "presets": [ 25 | [ 26 | "@babel/env", 27 | { 28 | "modules": "commonjs", 29 | "exclude": ["@babel/plugin-transform-regenerator"] 30 | } 31 | ] 32 | ] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["react-app", "prettier"], 4 | "env": { 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | "sourceType": "module" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tannerlinsley 2 | custom: https://youtube.com/tannerlinsley 3 | -------------------------------------------------------------------------------- /.github/size-plugin.yml: -------------------------------------------------------------------------------- 1 | size-files: 2 | - sizes-cjs.json 3 | - sizes-es.json 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | artifacts 11 | .rpt2_cache 12 | 13 | # misc 14 | .DS_Store 15 | .env 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .next 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | .history -------------------------------------------------------------------------------- /.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "dist/index.js": { 3 | "bundled": 29796, 4 | "minified": 14847, 5 | "gzipped": 4213 6 | }, 7 | "dist/index.es.js": { 8 | "bundled": 29276, 9 | "minified": 14383, 10 | "gzipped": 4111, 11 | "treeshaked": { 12 | "rollup": { 13 | "code": 3472, 14 | "import_statements": 21 15 | }, 16 | "webpack": { 17 | "code": 4493 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.24 4 | 5 | - Fixed an issue where isDocumentVisible wasn't properly guarded against in all non-web scenarios 6 | - Fixed an issue where query cancellation functions may not have been called 7 | - Added the new `setFocusHandler` utility which allows the overriding of the event that triggers window focusing 8 | - Updated the docs to show how to use `setFocusHandler` to avoid iframe events from triggerig window focus 9 | 10 | ## 0.3.23 11 | 12 | - Fixed an issue where queries would not refresh in the background when using suspense 13 | 14 | ## 0.3.22 15 | 16 | - Caching is now disabled when React Query is used on the server. It is still possible to seed queries using `initialData` during SSR. 17 | 18 | ## 0.3.21 19 | 20 | - Fixed an edge case where `useIsLoading` would not update or rerender correctly. 21 | 22 | ## 0.3.20 23 | 24 | - Added `config.refetchIntervalInBackground` option 25 | 26 | ## 0.3.19 27 | 28 | - Added `config.initialData` option for SSR 29 | 30 | ## 0.3.18 31 | 32 | - Fix and issue where `setQueryData` would crash when the query does not exist 33 | 34 | ## 0.3.17 35 | 36 | - Fix and issue where queries would double fetch when using suspense 37 | 38 | ## 0.3.16 39 | 40 | - Remove nodent runtime from react-async (shaved off 938 bytes!) 41 | 42 | ## 0.3.15 43 | 44 | - Better esm bundle configuration 45 | 46 | ## 0.3.14 47 | 48 | - Add `promise.cancel` support to query promises to support request cancellation APIs 49 | - Refetch all on window focus should no longer raise unhandled promise rejections 50 | 51 | ## 0.3.13 52 | 53 | - Fix issue where `document` was not guarded againts in React Native 54 | 55 | ## 0.3.12 56 | 57 | - Remove orphaned npm dependencies 58 | 59 | ## 0.3.11 60 | 61 | - Add `@types/react-query` as a dependency for typescript users 62 | 63 | ## 0.3.10 64 | 65 | - Fix issue where window focus event would try and register in react-native 66 | 67 | ## 0.3.9 68 | 69 | - Fix issue where variable hashes could contain arrays or similar number/string pairs 70 | - Fix issue where clearing query cache could lead to out of date query states 71 | 72 | ## 0.3.8 73 | 74 | - Internal cleanup and refactoring 75 | 76 | ## 0.3.7 77 | 78 | - Added the `clearQueryCache` API function to clear the query cache 79 | 80 | ## 0.3.6 81 | 82 | - Fixed an issue where passing `config` to `ReactQueryConfigProvider` would not update the non-hook `defaultContext` 83 | 84 | ## 0.3.5 85 | 86 | - Fixed an issue where `isLoading` would remain `true` if a query encountered an error after all retries 87 | - Fixed regression where `useIsFetching` stopped working 88 | 89 | ## 0.3.4 90 | 91 | - Fixed an issue where `useMutation().mutate` would not throw an error when failing 92 | 93 | ## 0.3.3 94 | 95 | - Fixed an issue where falsey query keys would sometimes still fetch 96 | 97 | ## 0.3.2 98 | 99 | - Added the `useQuery.onSuccess` callback option 100 | - Added the `useQuery.onError` callback option 101 | 102 | ## 0.3.1 103 | 104 | - Added the `prefetchQuery` method 105 | - Improved support for Suspense including fetch-as-you-render patterns 106 | - Undocumented `_useQueries` hook has been removed 107 | 108 | ## 0.3.0 109 | 110 | - The `useReactQueryConfig` hook is now a provider component called `ReactQueryConfigProvider` 111 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tanner Linsley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![React Query Header](https://github.com/tannerlinsley/react-query/raw/master/media/header.png) 2 | 3 | 4 | 5 | Hooks for fetching, caching and updating asynchronous data in React 6 | 7 | 10 | 11 | #TanStack 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Join the community on Spectrum 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Enjoy this library? Try them all! [React Table](https://github.com/tannerlinsley/react-table), [React Form](https://github.com/tannerlinsley/react-form), [React Charts](https://github.com/tannerlinsley/react-charts) 30 | 31 | ## Quick Features 32 | 33 | - Transport, protocol & backend agnostic data fetching 34 | - Auto Caching + Refetching (stale-while-revalidate, Window Refocus, Polling/Realtime) 35 | - Parallel + Dependent Queries 36 | - Mutations + Automatic Query Refetching 37 | - Multi-layer Cache + Garbage Collection 38 | - Load-More Pagination + Scroll Recovery 39 | - Request Cancellation 40 | - [React Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html) Support 41 | - 42 | 43 | 44 | 45 |
46 | Core Issues and Solution 47 | 48 | ## The Challenge 49 | 50 | Tools for managing async data and client stores/caches are plentiful these days, but most of these tools: 51 | 52 | - Duplicate unnecessary network operations 53 | - Force normalized or object/id-based caching strategies on your data 54 | - Do not automatically manage stale-ness or caching 55 | - Do not offer robust API's around mutation events, invalidation or query management 56 | - Are built for highly-opinionated systems like Redux, GraphQL, [insert proprietary tools] etc. 57 | 58 | ## The Solution 59 | 60 | React Query exports a set of hooks that attempt to address these issues. Out of the box, React Query: 61 | 62 | - Flexibly dedupes simultaneous requests to assets 63 | - Automatically caches data 64 | - Automatically invalidates stale cache data 65 | - Optimistically updates stale requests in the background 66 | - Automatically manages garbage collection 67 | - Supports automatic retries and exponential or custom back-off delays 68 | - Provides both declarative and imperative API's for: 69 | - Mutations and automatic query syncing 70 | - Query Refetching 71 | - Atomic and Optimistic query manipulation 72 | 73 |
74 | 75 |
76 | Inspiration & Hat-Tipping 77 |
78 | A big thanks to both [Draqula](https://github.com/vadimdemedes/draqula) for inspiring a lot of React Query's original API and documentation and also [Zeit's SWR](https://github.com/zeit/swr) and its creators for inspiring even further customizations and examples. You all rock! 79 | 80 |
81 | 82 | ## Examples 83 | 84 | - [Basic](./examples/basic) 85 | - [Custom Hooks](./examples/custom-hooks) 86 | - [Auto Refetching / Polling / Realtime](./examples/auto-refetching) 87 | - [Window Refocus Refetching](./examples/focus-refetching) 88 | - [Optimistic Updates](./examples/optimistic-updates) 89 | - [Load-More Pagination](./examples/load-more-pagination) 90 | - [Suspense CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/suspense) 91 | - [Playground CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/sandbox) 92 | 93 | ## Sponsors 94 | 95 | This library is being built and maintained by me, @tannerlinsley and I am always in need of more support to keep projects like this afloat. If you would like to get premium support, add your logo or name on this README, or simply just contribute to my open source Sponsorship goal, [visit my Github Sponsors page!](https://github.com/sponsors/tannerlinsley/) 96 | 97 | 98 | 99 | 100 | 105 | 110 | 111 | 112 |
101 | 102 | 103 | 104 | 106 | 107 | Become a Sponsor! 108 | 109 |
113 | 114 | 115 | 116 | 117 | 122 | 127 | 128 | 129 |
118 | 119 | 120 | 121 | 123 | 124 | Become a Sponsor! 125 | 126 |
130 | 131 | 132 | 133 | 134 | 139 | 144 | 145 | 146 |
135 | 136 | 137 | 138 | 140 | 141 | Become a Sponsor! 142 | 143 |
147 | 148 | 149 | 150 | 151 | 156 | 161 | 162 | 163 |
152 | 153 | 154 | 155 | 157 | 158 | Become a Sponsor! 159 | 160 |
164 | 165 | 166 | 167 | 168 | 173 | 178 | 183 | 184 | 185 |
169 | 170 | 171 | 172 | 174 | 177 | 179 | 180 | Become a Supporter! 181 | 182 |
186 | 187 | 188 | 189 | 190 | 195 | 200 | 205 | 206 | 207 |
191 | 192 | 193 | 194 | 201 | 202 | Become a Fan! 203 | 204 |
208 | 209 | # Documentation 210 | 211 | - [Installation](#installation) 212 | - [Queries](#queries) 213 | - [Query Keys](#query-keys) 214 | - [Query Variables](#query-variables) 215 | - [Dependent Queries](#dependent-queries) 216 | - [Caching & Invalidation](#caching--invalidation) 217 | - [Load-More & Infinite-Scroll Pagination](#load-more--infinite-scroll-pagination) 218 | - [Scroll Restoration](#scroll-restoration) 219 | - [Manual Querying](#manual-querying) 220 | - [Retries](#retries) 221 | - [Retry Delay](#retry-delay) 222 | - [Prefetching](#prefetching) 223 | - [SSR & Initial Data](#ssr--initial-data) 224 | - [Suspense Mode](#suspense-mode) 225 | - [Fetch-on-render vs Fetch-as-you-render](#fetch-on-render-vs-fetch-as-you-render) 226 | - [Cancelling Query Requests](#cancelling-query-requests) 227 | - [Mutations](#mutations) 228 | - [Basic Mutations](#basic-mutations) 229 | - [Mutation Variables](#mutation-variables) 230 | - [Invalidate and Refetch Queries from Mutations](#invalidate-and-refetch-queries-from-mutations) 231 | - [Query Updates from Mutations](#query-updates-from-mutations) 232 | - [Manually or Optimistically Setting Query Data](#manually-or-optimistically-setting-query-data) 233 | - [Displaying Background Fetching Loading States](#displaying-background-fetching-loading-states) 234 | - [Displaying Global Background Fetching Loading State](#displaying-global-background-fetching-loading-state) 235 | - [Window-Focus Refetching](#window-focus-refetching) 236 | - [Custom Window Focus Event](#custom-window-focus-event) 237 | - [Ignoring Iframe Focus Events](#ignoring-iframe-focus-events) 238 | - [Custom Query Key Serializers (Experimental)](#custom-query-key-serializers-experimental) 239 | - [API](#api) 240 | - [`useQuery`](#usequery) 241 | - [`useMutation`](#usemutation) 242 | - [`setQueryData`](#setquerydata) 243 | - [`refetchQuery`](#refetchquery) 244 | - [`prefetchQuery`](#prefetchquery) 245 | - [`refetchAllQueries`](#refetchallqueries) 246 | - [`useIsFetching`](#useisfetching) 247 | - [`clearQueryCache`](#clearquerycache) 248 | - [`ReactQueryConfigProvider`](#reactqueryconfigprovider) 249 | 250 | ## Installation 251 | 252 | ```bash 253 | $ npm i --save react-query 254 | # or 255 | $ yarn add react-query 256 | ``` 257 | 258 | ## Queries 259 | 260 | To make a new query, call the `useQuery` hook with: 261 | 262 | - A **unique key for the query** 263 | - An **asynchronous function (or similar then-able)** to resolve the data 264 | 265 | ```js 266 | const info = useQuery('todos', fetchTodoList) 267 | ``` 268 | 269 | The **unique key** you provide is used internally for refetching, caching, deduping related queries. 270 | 271 | This key can be whatever you'd like it to be as long as: 272 | 273 | - It changes when your query should be requested again 274 | - It is consistent across all instances of that specific query in your application 275 | 276 | The query `info` returned contains all information about the query and can be easily destructured and used in your component: 277 | 278 | ```js 279 | function Todos() { 280 | const { data, isLoading, error } = useQuery('todos', fetchTodoList) 281 | 282 | return ( 283 |
284 | {isLoading ? ( 285 | Loading... 286 | ) : error ? ( 287 | Error: {error.message} 288 | ) : data ? ( 289 | 294 | ) : null} 295 |
296 | ) 297 | } 298 | ``` 299 | 300 | ### Query Keys 301 | 302 | Since React Query uses a query's **unique key** for essentially everything, it's important to tailor them so that will change with your query requirements. In other libraries like Zeit's SWR, you'll see the use of URL's and GraphQL query template strings to achieve this, but we believe at scale, this becomes prone to typos and errors. To relieve this issue, you can pass a **tuple key** with a `string` and `object` of variables to deterministically get the the same key. 303 | 304 | > Pro Tip: Variables passed in the key are automatically passed to your query function! 305 | 306 | All of the following queries would result in using the same key: 307 | 308 | ```js 309 | useQuery(['todos', { status, page }]) 310 | useQuery(['todos', { page, status }]) 311 | useQuery(['todos', { page, status, other: undefined }]) 312 | ``` 313 | 314 | ### Query Variables 315 | 316 | To use external props, state, or variables in a query function, pass them as a variables in your query key! They will be passed through to your query function as the first parameter. 317 | 318 | ```js 319 | function Todos({ status }) { 320 | const { data, isLoading, error } = useQuery( 321 | ['todos', { status, page }], 322 | fetchTodoList // This is the same as `fetchTodoList({ status, page })` 323 | ) 324 | } 325 | ``` 326 | 327 | Whenever a query's key changes, the query will automatically update: 328 | 329 | ```js 330 | function Todos() { 331 | const [page, setPage] = useState(0) 332 | 333 | const { data, isLoading, error } = useQuery( 334 | ['todos', { page }], 335 | fetchTodoList 336 | ) 337 | 338 | const onNextPage = () => { 339 | setPage(page => page + 1) 340 | } 341 | 342 | return ( 343 | <> 344 | {/* ... */} 345 | 346 | 347 | ) 348 | } 349 | ``` 350 | 351 | ### Dependent Queries 352 | 353 | React Query makes it easy to make queries that depend on other queries for both: 354 | 355 | - Parallel Queries (avoiding waterfalls) and 356 | - Serial Queries (when a piece of data is required for the next query to happen). 357 | 358 | To do this, you can use the following 2 approaches: 359 | 360 | #### Pass a falsey query key 361 | 362 | If a query isn't ready to be requested yet, just pass a falsey value as the query key: 363 | 364 | ```js 365 | const { data: user } = useQuery(['user', { userId }]) 366 | const { data: projects } = useQuery(user && ['projects', { userId: user.id }]) // User is `null`, so the query key will be falsey 367 | ``` 368 | 369 | #### Use a query key function 370 | 371 | If a function is passed, the query will not execute until the function can be called without throwing: 372 | 373 | ```js 374 | const { data: user } = useQuery(['user', { userId }]) 375 | const { data: projects } = useQuery(() => ['projects', { userId: user.id }]) // This will throw until `user` is available 376 | ``` 377 | 378 | #### Mix them together! 379 | 380 | ```js 381 | const [ready, setReady] = React.useState(false) 382 | const { data: user } = useQuery(ready && ['user', { userId }]) // Wait for ready to be truthy 383 | const { data: projects } = useQuery( 384 | () => ['projects', { userId: user.id }] // Wait for user.id to become available (and not throw) 385 | ``` 386 | 387 | ### Caching & Invalidation 388 | 389 | React Query caching is automatic out of the box. It uses a `stale-while-revalidate` in-memory caching strategy together with robust query deduping to always ensure a query's data is only cached when it's needed and only cached once even if that query is used multiple times across your application. 390 | 391 | At a glance: 392 | 393 | - The cache is keyed on unique `query + variables` combinations. 394 | - By default query results become **stale** immediately after a successful fetch. This can be configured using the `staleTime` option at both the global and query-level. 395 | - Stale queries are automatically refetched whenever their **query keys change (this includes variables used in query key tuples)** or when **new usages/instances** of a query are mounted. 396 | - By default query results are **always** cached **when in use**. 397 | - If and when a query is no longer being used, it becomes **inactive** and by default is cached in the background for **5 minutes**. This time can be configured using the `cacheTime` option at both the global and query-level. 398 | - After a query is inactive for the `cacheTime` specified (defaults to 5 minutes), the query is deleted and garbage collected. 399 | 400 |
401 | A more detailed example of the caching lifecycle 402 | 403 | Let's assume we are using the default `cacheTime` of **5 minutes** and the default `staleTime` of `0`. 404 | 405 | - A new instance of `useQuery('todos', fetchTodos)` mounts. 406 | - Since no other queries have been made with this query + variable combination, this query will show a hard loading state and make a network request to fetch the data. 407 | - It will then cache the data using `'todos'` and `` as the unique identifiers for that cache. 408 | - A stale invalidation is scheduled using the `staleTime` option as a delay (defaults to `0`, or immediately). 409 | - A second instance of `useQuery('todos', fetchTodos)` mounts elsewhere. 410 | - Because this exact data exist in the cache from the first instance of this query, that data is immediately returned from the cache. 411 | - Since the query is stale, it is refetched in the background automatically. 412 | - Both instances of the `useQuery('todos', fetchTodos)` query are unmount and no longer in use. 413 | - Since there are no more active instances to this query, a cache timeout is set using `cacheTime` to delete and garbage collect the query (defaults to **5 minutes**). 414 | - No more instances of `useQuery('todos', fetchTodos)` appear within **5 minutes**. 415 | - This query and its data is deleted and garbage collected. 416 | 417 |
418 | 419 | ### Load-More & Infinite-Scroll Pagination 420 | 421 | Rendering paginated lists that can "load more" data or "infinite scroll" is a common UI pattern. React Query supports some useful features for querying these types of lists. Let's assume we have an API that returns pages of `todos` 3 at a time based on a `cursor` index: 422 | 423 | ```js 424 | fetch('/api/projects?cursor=0') 425 | // { data: [...], nextId: 3} 426 | fetch('/api/projects?cursor=3') 427 | // { data: [...], nextId: 6} 428 | fetch('/api/projects?cursor=6') 429 | // { data: [...], nextId: 9} 430 | ``` 431 | 432 | Using the `nextId` value in each page's response, we can configure `useQuery` to fetch more pages as needed: 433 | 434 | - Configure your query function to use optional pagination variables. We'll send through the `nextId` as the `cursor` for the next page request. 435 | - Set the `paginated` option to `true`. 436 | - Define a `getCanFetchMore` option to know if there is more data to load (it receives the `lastPage` and `allPages` as parameters). 437 | 438 | ```js 439 | import { useQuery } from 'react-query' 440 | 441 | function Todos() { 442 | const { 443 | data: pages, 444 | isLoading, 445 | isFetching, 446 | isFetchingMore, 447 | fetchMore, 448 | canFetchMore, 449 | } = useQuery( 450 | 'todos', 451 | ({ nextId } = {}) => fetch('/api/projects?cursor=' + (nextId || 0)), 452 | { 453 | paginated: true, 454 | getCanFetchMore: (lastPage, allPages) => lastPage.nextId, 455 | } 456 | ) 457 | 458 | // ... 459 | } 460 | ``` 461 | 462 | You'll notice a few new things now: 463 | 464 | - `data` is now an array of pages that contain query results, instead of the query results themselves 465 | - A `fetchMore` function is now available 466 | - A `canFetchMore` boolean is now available 467 | - An `isFetchingMore` boolean is now available 468 | 469 | These can now be used to render a "load more" list (this example uses an `offset` key): 470 | 471 | ```js 472 | import { useQuery } from 'react-query' 473 | 474 | function Todos() { 475 | const { 476 | data: pages, 477 | isLoading, 478 | isFetching, 479 | isFetchingMore, 480 | fetchMore, 481 | canFetchMore, 482 | } = useQuery( 483 | 'projects', 484 | ({ offset } = {}) => fetch('/api/projects?offset=' + (offset || 0)), 485 | { 486 | paginated: true, 487 | getCanFetchMore: (lastPage, allPages) => lastPage.nextId, 488 | } 489 | ) 490 | 491 | const loadMore = async () => { 492 | try { 493 | // Get the last page 494 | const lastPage = pages[pages.length - 1] 495 | const { nextId } = lastPage 496 | // Fetch more starting from nextId 497 | await fetchMore({ 498 | offset: nextId, 499 | }) 500 | } catch {} 501 | } 502 | 503 | return isLoading ? ( 504 |

Loading...

505 | ) : pages ? ( 506 | <> 507 | {pages.map((page, i) => ( 508 | 509 | {page.data.map(project => ( 510 |

{project.name}

511 | ))} 512 |
513 | ))} 514 |
515 | {canFetchMore ? ( 516 | 519 | ) : ( 520 | 'Nothing more to fetch.' 521 | )} 522 |
523 |
524 | {isFetching && !isFetchingMore ? 'Background Updating...' : null} 525 |
526 | 527 | ) : null 528 | } 529 | ``` 530 | 531 | #### What happens when a paginated query needs to be refetched?\*\* 532 | 533 | When a paginated query becomes `stale` and needs to be refetched, each page is fetched `individually` with the same variables that were used to request it originally. If a paginated query's results are ever removed from the cache, the pagination restarts at the initial state with a single page being requested. 534 | 535 | ### Scroll Restoration 536 | 537 | Out of the box, "scroll restoration" Just Works™️ in React Query. The reason for this is that query results are cached and retrieved synchronously when rendered. As long as a query is cached and has not been garbage collected, you should never experience problems with scroll restoration. 538 | 539 | ### Manual Querying 540 | 541 | If you ever want to disable a query from automatically running, you can use the `manual = true` option. When `manual` is set to true: 542 | 543 | - The query will not automatically refetch due to changes to their query function or variables. 544 | - The query will not automatically refetch due to `refetchQueries` options in other queries or via `refetchQuery` calls. 545 | 546 | ```js 547 | function Todos() { 548 | const { data, isLoading, error, refetch, isFetching } = useQuery( 549 | 'todos', 550 | fetchTodoList, 551 | { 552 | manual: true, 553 | } 554 | ) 555 | 556 | return ( 557 | <> 558 | 559 | 560 | {isLoading ? ( 561 | Loading... 562 | ) : error ? ( 563 | Error: {error.message} 564 | ) : data ? ( 565 | <> 566 | 571 | 572 | ) : null} 573 | 574 | ) 575 | } 576 | ``` 577 | 578 | > Pro Tip: Don't use `manual` for dependent queries. Use [Dependent Queries](#dependent-queries) instead! 579 | 580 | ### Retries 581 | 582 | When a `useQuery` query fails (the function throws an error), React Query will automatically retry the query if that query's request has not reached the max number of consecutive retries (defaults to `3`). 583 | 584 | You can configure retries both on a global level and an individual query level. 585 | 586 | - Setting `retry = false` will disable retries. 587 | - Setting `retry = 6` will retry failing requests 6 times before showing the final error thrown by the function. 588 | - Setting `retry = true` will infinitely retry failing requests. 589 | 590 | ```js 591 | import { useQuery } from 'react-query' 592 | 593 | // Make specific query retry a certain number of times 594 | const { data, isLoading, error } = useQuery( 595 | ['todos', { page: 1 }], 596 | fetchTodoList, 597 | { 598 | retry: 10, // Will retry failed requests 10 times before displaying an error 599 | } 600 | ) 601 | ``` 602 | 603 | ### Retry Delay 604 | 605 | By default, retries in React Query do not happen immediately after a request fails. As is standard, a back-off delay is gradually applied to each retry attempt. 606 | 607 | The default `retryDelay` is set to double (starting at `1000`ms) with each attempt, but not exceed 30 seconds: 608 | 609 | ```js 610 | // Configure for all queries 611 | import { ReactQueryConfigProvider } from 'react-query' 612 | 613 | const queryConfig = { 614 | retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), 615 | } 616 | 617 | function App() { 618 | return ( 619 | 620 | ... 621 | 622 | ) 623 | } 624 | ``` 625 | 626 | Though it is not recommended, you can obviously override the `retryDelay` function/integer in both the Provider and individual query options. If set to an integer instead of a function the delay will always be the same amount of time: 627 | 628 | ```js 629 | const { data, isLoading, error } = useQuery('todos', fetchTodoList, { 630 | retryDelay: 10000, // Will always wait 1000ms to retry, regardless of how many retries 631 | }) 632 | ``` 633 | 634 | ### Prefetching 635 | 636 | If you're lucky enough, you may know enough about what your users will do to be able to prefetch the data they need before it's needed! If this is the case, then you're in luck. You can use the `prefetchQuery` function to prefetch the results of a query to be placed into the cache: 637 | 638 | ```js 639 | import { prefetchQuery } from 'react-query' 640 | 641 | const prefetchTodos = async () => { 642 | const queryData = await prefetchQuery('todos', () => fetch('/todos')) 643 | // The results of this query will be cached like a normal query 644 | } 645 | ``` 646 | 647 | The next time a `useQuery` instance is used for a prefetched query, it will use the cached data! If no instances of `useQuery` appear for a prefetched query, it will be deleted and garbage collected after the time specified in `cacheTime`. 648 | 649 | ### SSR & Initial Data 650 | 651 | When using SSR (server-side-rendering) with React Query there are a few things to note: 652 | 653 | - Caching is not performed during SSR. This is outside of the scope of React Query and easily leads to out-of-sync data when used with frameworks like Next.js or other SSR strategies. 654 | - Queries rendered on the server will by default use the initial state of an unfetched query. This means that `data` will be set to `null`. To get around this in SSR, you can pre-seed a query's data using the `config.initialData` option: 655 | 656 | ```js 657 | const { data, isLoading, error } = useQuery('todos', fetchTodoList, { 658 | initialData: [{ id: 0, name: 'Implement SSR!' }], 659 | }) 660 | 661 | // data === [{ id: 0, name: 'Implement SSR!'}] 662 | ``` 663 | 664 | The query's state will still reflect that it is stale and has not been fetched yet, and once mounted, will continue as normal and request a fresh copy of the query result. 665 | 666 | ### Suspense Mode 667 | 668 | React Query can also be used with React's new Suspense for Data Fetching API's. To enable this mode, you can set either the global or query level config's `suspense` option to `true`. 669 | 670 | Global configuration: 671 | 672 | ```js 673 | // Configure for all queries 674 | import { ReactQueryConfigProvider } from 'react-query' 675 | 676 | const queryConfig = { 677 | suspense: true, 678 | } 679 | 680 | function App() { 681 | return ( 682 | 683 | ... 684 | 685 | ) 686 | } 687 | ``` 688 | 689 | Query configuration: 690 | 691 | ```js 692 | const { useQuery } from 'react-query' 693 | 694 | // Enable for an individual query 695 | useQuery(queryKey, queryFn, { suspense: true }) 696 | ``` 697 | 698 | When using suspense mode, `isLoading` and `error` states will be replaced by usage of the `React.Suspense` component (including the use of the `fallback` prop and React error boundaries for catching errors). Please see the [Suspense Example](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/sandbox) for more information on how to set up suspense mode. 699 | 700 | ### Fetch-on-render vs Fetch-as-you-render 701 | 702 | Out of the box, React Query in `suspense` mode works really well as a **Fetch-on-render** solution with no additional configuration. However, if you want to take it to the next level and implement a `Fetch-as-you-render` model, we recommend implementing [Prefetching](#prefetching) on routing and/or user interactions events to initialize queries before they are needed. 703 | 704 | ### Cancelling Query Requests 705 | 706 | By default, queries that become inactive before their promises are resolved are simply ignored instead of cancelled. Why is this? 707 | 708 | - For most applications, ignoring out-of-date queries is sufficient. 709 | - Cancellation APIs may not be available for every query function. 710 | - If cancellation APIs are available, they typically vary in implementation between utilities/libraries (eg. Fetch vs Axios vs XMLHttpRequest). 711 | 712 | But don't worry! If your queries are high-bandwidth or potentially very expensive to download, React Query exposes a generic way to **cancel** query requests using a cancellation token or other related API. To integrate with this feature, attach a `cancel` function to the promise returned by your query that implements your request cancellation. When a query becomes out-of-date or inactive, this `promise.cancel` function will be called (if available): 713 | 714 | Using `axios`: 715 | 716 | ```js 717 | import { CancelToken } from 'axios' 718 | 719 | const query = useQuery('todos', () => { 720 | // Create a new CancelToken source for this request 721 | const source = CancelToken.source() 722 | 723 | const promise = axios.get('/todos', { 724 | // Pass the source token to your request 725 | cancelToken: source.token, 726 | }) 727 | 728 | // Cancel the request if React Query calls the `promise.cancel` method 729 | promise.cancel = () => { 730 | source.cancel('Query was cancelled by React Query') 731 | } 732 | 733 | return promise 734 | }) 735 | ``` 736 | 737 | Using `fetch`: 738 | 739 | ```js 740 | const query = useQuery('todos', () => { 741 | // Create a new AbortController instance for this request 742 | const controller = new AbortController() 743 | // Get the abortController's signal 744 | const signal = controller.signal 745 | 746 | const promise = fetch('/todos', { 747 | method: 'get', 748 | // Pass the signal to your request 749 | signal, 750 | }) 751 | 752 | // Cancel the request if React Query calls the `promise.cancel` method 753 | promise.cancel = controller.abort 754 | 755 | return promise 756 | }) 757 | ``` 758 | 759 | ## Mutations 760 | 761 | Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, React Query exports a `useMutation` hook. 762 | 763 | ### Basic Mutations 764 | 765 | Assuming the server implements a ping mutation, that returns "pong" string, here's an example of the most basic mutation: 766 | 767 | ```js 768 | const PingPong = () => { 769 | const [mutate, { data, isLoading, error }] = useMutation(pingMutation) 770 | 771 | const onPing = async () => { 772 | try { 773 | const data = await mutate() 774 | console.log(data) 775 | // { ping: 'pong' } 776 | } catch { 777 | // Uh oh, something went wrong 778 | } 779 | } 780 | return 781 | } 782 | ``` 783 | 784 | Mutations without variables are not that useful, so let's add some variables to closer match reality. 785 | 786 | ### Mutation Variables 787 | 788 | To pass `variables` to your `mutate` function, call `mutate` with an object. 789 | 790 | ```js 791 | const CreateTodo = () => { 792 | const [title, setTitle] = useState('') 793 | const [mutate] = useMutation(createTodo) 794 | 795 | const onCreateTodo = async e => { 796 | // Prevent the form from refreshing the page 797 | e.preventDefault() 798 | 799 | try { 800 | await mutate({ title }) 801 | // Todo was successfully created 802 | } catch (error) { 803 | // Uh oh, something went wrong 804 | } 805 | } 806 | 807 | return ( 808 |
809 | setTitle(e.target.value)} 813 | /> 814 |
815 | 816 |
817 | ) 818 | } 819 | ``` 820 | 821 | Even with just variables, mutations aren't all that special, but when used with the `refetchQueries` and `updateQuery` options, they become a very powerful tool. 822 | 823 | ### Invalidate and Refetch Queries from Mutations 824 | 825 | When a mutation succeeds, it's likely that other queries in your application need to update. Where other libraries that use normalized caches would attempt to update locale queries with the new data imperatively, React Query avoids the pitfalls that come with normalized caches and prescribes **atomic updates** instead of partial cache manipulation. 826 | 827 | For example, assume we have a mutation to post a new todo: 828 | 829 | ```js 830 | const [mutate] = useMutation(postTodo) 831 | ``` 832 | 833 | When a successful `postTodo` mutation happens, we likely want all `todos` queries to get refetched to show the new todo item. To do this, you can use the `refetchQueries` option when calling a mutation's `mutate` function. 834 | 835 | ```js 836 | // When this mutation succeeds, any queries with the `todos` or `reminders` query key will be refetched 837 | const [mutate] = useMutation(addTodo, { 838 | refetchQueries: ['todos', 'reminders'], 839 | }) 840 | const run = async () => { 841 | try { 842 | await mutate(todo) 843 | } catch {} 844 | } 845 | 846 | // The 3 queries below will be refetched when the mutation above succeeds 847 | const todoListQuery = useQuery('todos', fetchTodoList) 848 | const todoListQuery = useQuery(['todos', { page: 1 }], fetchTodoList) 849 | const remindersQuery = useQuery('reminders', fetchReminders) 850 | ``` 851 | 852 | You can even refetch queries with specific variables by passing a query key tuple to `refetchQueries`: 853 | 854 | ```js 855 | const [mutate] = useMutation(addTodo, { 856 | refetchQueries: [['todos', { status: 'done' }]], 857 | }) 858 | const run = async () => { 859 | try { 860 | await mutate(todo) 861 | } catch {} 862 | } 863 | 864 | // The query below will be refetched when the mutation above succeeds 865 | const todoListQuery = useQuery(['todos', { status: 'done' }], fetchTodoList) 866 | // However, the following query below will NOT be refetched 867 | const todoListQuery = useQuery('todos', fetchTodoList) 868 | ``` 869 | 870 | If you want to **only** refetch `todos` queries that don't have variables, you can pass a tuple with `variables` set to `false`: 871 | 872 | ```js 873 | const [mutate] = useMutation(addTodo, { refetchQueries: [['todos', false]] }) 874 | const run = async () => { 875 | try { 876 | await mutate(todo) 877 | } catch {} 878 | } 879 | 880 | // The query below will be refetched when the mutation above succeeds 881 | const todoListQuery = useQuery(['todos'], fetchTodoList) 882 | // However, the following query below will NOT be refetched 883 | const todoListQuery = useQuery(['todos', { status: 'done' }], fetchTodoList) 884 | ``` 885 | 886 | If you prefer that the promise returned from `mutate()` only resolves **after** any `refetchQueries` have been refetched, you can pass the `waitForRefetchQueries = true` option to `mutate`: 887 | 888 | ```js 889 | const [mutate] = useMutation(addTodo, { refetchQueries: ['todos'] }) 890 | 891 | const run = async () => { 892 | try { 893 | await mutate(todo, { waitForRefetchQueries: true }) 894 | console.log('I will only log after all refetchQueries are done refetching!') 895 | } catch {} 896 | } 897 | ``` 898 | 899 | It's important to note that `refetchQueries` by default will only happen after a successful mutation (the mutation function doesn't throw an error). If you would like to refetch the `refetchQueries` regardless of this, you can set `refetchQueriesOnFailure` to `true` in your `mutate` options: 900 | 901 | ```js 902 | const [mutate] = useMutation(addTodo, { refetchQueries: ['todos'] }) 903 | 904 | const run = async () => { 905 | try { 906 | await mutate(todo, { refetchQueriesOnFailure: true }) 907 | // Even if the above mutation fails, any `todos` queries will still be refetched. 908 | } catch {} 909 | } 910 | ``` 911 | 912 | ### Query Updates from Mutations 913 | 914 | When dealing with mutations that **update** objects on the server, it's common for the new object to be automatically returned in the response of the mutation. Instead of invalidating any queries for that item and wasting a network call to refetch them again, we can take advantage of the object returned by the mutation function and update any query responses with that data that match that query using the `updateQuery` option: 915 | 916 | ```js 917 | const [mutate] = useMutation(editTodo) 918 | 919 | mutate( 920 | { 921 | id: 5, 922 | name: 'Do the laundry', 923 | }, 924 | { 925 | updateQuery: ['todo', { id: 5 }], 926 | } 927 | ) 928 | 929 | // The query below will be updated with the response from the mutation above when it succeeds 930 | const { data, isLoading, error } = useQuery(['todo', { id: 5 }], fetchTodoByID) 931 | ``` 932 | 933 | ## Manually or Optimistically Setting Query Data 934 | 935 | In rare circumstances, you may want to manually update a query's response before it has been refetched. To do this, you can use the exported `setQueryData` function: 936 | 937 | ```js 938 | import { setQueryData } from 'react-query' 939 | 940 | // Full replacement 941 | setQueryData(['todo', { id: 5 }], newTodo) 942 | 943 | // or functional update 944 | setQueryData(['todo', { id: 5 }], previous => ({ ...previous, status: 'done' })) 945 | ``` 946 | 947 | **Most importantly**, when manually setting a query response, it naturally becomes out-of-sync with it's original source. To ease this issue, `setQueryData` automatically triggers a background refresh of the query after it's called to ensure it eventually synchronizes with the original source. 948 | 949 | Should you choose that you do _not_ want to refetch the query automatically, you can set the `shouldRefetch` option to `false`: 950 | 951 | ```js 952 | import { setQueryData } from 'react-query' 953 | 954 | // Mutate, but do not automatically refetch the query in the background 955 | setQueryData(['todo', { id: 5 }], newTodo, { 956 | shouldRefetch: false, 957 | }) 958 | ``` 959 | 960 | ## Displaying Background Fetching Loading States 961 | 962 | A query's `isLoading` boolean is usually sufficient to show the initial hard-loading state for a query, but sometimes you may want to display a more subtle indicator that a query is refetching in the background. To do this, queries also supply you with an `isFetching` boolean that you can use to show that it's in a fetching state: 963 | 964 | ```js 965 | function Todos() { 966 | const { data: todos, isLoading, isFetching } = useQuery('todos', fetchTodos) 967 | 968 | return isLoading ? ( 969 | Loading... 970 | ) : todos ? ( 971 | <> 972 | {isFetching ?
Refreshing...
: null} 973 | 974 |
975 | {todos.map(todo => ( 976 | 977 | ))} 978 |
979 | 980 | ) : null 981 | } 982 | ``` 983 | 984 | ## Displaying Global Background Fetching Loading State 985 | 986 | In addition to individual query loading states, if you would like to show a global loading indicator when **any** queries are fetching (including in the background), you can use the `useIsFetching` hook: 987 | 988 | ```js 989 | import { useIsFetching } from 'react-query' 990 | 991 | function GlobalLoadingIndicator() { 992 | const isFetching = useIsFetching() 993 | 994 | return isFetching ? ( 995 |
Queries are fetching in the background...
996 | ) : null 997 | } 998 | ``` 999 | 1000 | ## Window-Focus Refetching 1001 | 1002 | If a user leaves your application and returns to stale data, you may want to trigger an update in the background to update any stale queries. Thankfully, **React Query does this automatically for you**, but if you choose to disable it, you can use the `ReactQueryConfigProvider`'s `refetchAllOnWindowFocus` option to disable it: 1003 | 1004 | ```js 1005 | const queryConfig = { refetchAllOnWindowFocus: false } 1006 | 1007 | function App() { 1008 | return ( 1009 | 1010 | ... 1011 | 1012 | ) 1013 | } 1014 | ``` 1015 | 1016 | ### Custom Window Focus Event 1017 | 1018 | In rare circumstances, you may want manage your own window focus events that trigger React Query to revalidate. To do this, React Query provides a `setFocusHandler` function that supplies you the callback that should be fired when the window is focused and allows you to set up your own events. When calling `setFocusHandler`, the previously set handler is removed (which in most cases will be the defalt handler) and your new handler is used instead. For example, this is the default handler: 1019 | 1020 | ```js 1021 | setFocusHandler(handleFocus => { 1022 | // Listen to visibillitychange and focus 1023 | if (typeof window !== 'undefined' && window.addEventListener) { 1024 | window.addEventListener('visibilitychange', handleFocus, false) 1025 | window.addEventListener('focus', handleFocus, false) 1026 | } 1027 | 1028 | return () => { 1029 | // Be sure to unsubscribe if a new handler is set 1030 | window.removeEventListener('visibilitychange', handleFocus) 1031 | window.removeEventListener('focus', handleFocus) 1032 | } 1033 | }) 1034 | ``` 1035 | 1036 | ### Ignoring Iframe Focus Events 1037 | 1038 | A greate use-case for replacing the focus handler is that of iframe events. Iframes present problems with detecting window focus by both double-firing events and also firing false-positive events when focusing or using iframes within your app. If you experience this, you should use an event handler that ignores these events as much as possible. I recommend [this one](https://gist.github.com/tannerlinsley/1d3a2122332107fcd8c9cc379be10d88)! It can be set up in the following way: 1039 | 1040 | ```js 1041 | import { setFocusHandler } from 'react-query' 1042 | import onWindowFocus from './onWindowFocus' // The gist above 1043 | 1044 | setFocusHandler(onWindowFocus) // Boom! 1045 | ``` 1046 | 1047 | ## Custom Query Key Serializers (Experimental) 1048 | 1049 | > **WARNING:** This is an advanced and experimental feature. There be dragons here. Do not change the Query Key Serializer unless you know what you are doing and are fine with encountering edge cases in the React Query API 1050 | 1051 | If you absolutely despise the default query key and variable syntax, you can replace the default query key serializer with your own by using the `ReactQueryConfigProvider` hook's `queryKeySerializerFn` option: 1052 | 1053 | ```js 1054 | const queryConfig = { 1055 | queryKeySerializerFn: userQueryKey => { 1056 | // Your custom logic here... 1057 | 1058 | return [fullQueryHash, queryGroupId, variablesHash, variables] 1059 | }, 1060 | } 1061 | 1062 | function App() { 1063 | return ( 1064 | 1065 | ... 1066 | 1067 | ) 1068 | } 1069 | ``` 1070 | 1071 | - `userQueryKey: any` 1072 | - This is the queryKey passed in `useQuery` and all other public methods and utilities exported by React Query. 1073 | - `fullQueryHash: string` 1074 | - This must be a unique `string` representing the query and variables. 1075 | - It must be stable and deterministic and should not change if things like the order of variables is changed or shuffled. 1076 | - `queryGroupId: string` 1077 | - This must be a unique `string` representing only the query type without any variables. 1078 | - It must be stable and deterministic and should not change if the variables of the query change. 1079 | - `variablesHash: string` 1080 | - This must be a unique `string` representing only the variables of the query. 1081 | - It must be stable and deterministic and should not change if things like the order of variables is changed or shuffled. 1082 | - `variables: any` 1083 | - This is the object that will be passed to the `queryFn` when using `useQuery`. 1084 | 1085 | > An additional `stableStringify` utility is also exported to help with stringifying objects to have sorted keys. 1086 | 1087 | #### URL Query Key Serializer Example 1088 | 1089 | The example below shows how to build your own serializer for use with urls and use it with React Query: 1090 | 1091 | ```js 1092 | import { ReactQueryConfigProvider, stableStringify } from 'react-query' 1093 | 1094 | function urlQueryKeySerializer(queryKey) { 1095 | // Deconstruct the url 1096 | let [url, params = ''] = queryKey.split('?') 1097 | 1098 | // Build the variables object 1099 | let variables = {} 1100 | params 1101 | .split('&') 1102 | .filter(Boolean) 1103 | .forEach(param => { 1104 | const [key, value] = param.split('=') 1105 | variables[key] = value 1106 | }) 1107 | 1108 | // Use stableStringify to turn variables into a stable string 1109 | const variablesHash = Object.keys(variables).length 1110 | ? stableStringify(variables) 1111 | : '' 1112 | 1113 | // Remove trailing slashes from the url to make an ID 1114 | const queryGroupId = url.replace(/\/{1,}$/, '') 1115 | 1116 | const queryHash = `${id}_${variablesHash}` 1117 | 1118 | return [queryHash, queryGroupId, variablesHash, variables] 1119 | } 1120 | 1121 | const queryConfig = { 1122 | queryKeySerializerFn: urlQueryKeySerializer, 1123 | } 1124 | 1125 | function App() { 1126 | return ( 1127 | 1128 | ... 1129 | 1130 | ) 1131 | } 1132 | 1133 | // Heck, you can even make your own custom useQueryHook! 1134 | 1135 | function useUrlQuery(url, options) { 1136 | return useQuery(url, () => axios.get(url).then(res => res.data)) 1137 | } 1138 | 1139 | // Use it in your app! 1140 | 1141 | function Todos() { 1142 | const todosQuery = useUrlQuery(`/todos`) 1143 | } 1144 | 1145 | function FilteredTodos({ status = 'pending' }) { 1146 | const todosQuery = useFunctionQuery([getTodos, { status }]) 1147 | } 1148 | 1149 | function Todo({ id }) { 1150 | const todoQuery = useUrlQuery(`/todos/${id}`) 1151 | } 1152 | 1153 | refetchQuery('/todos') 1154 | refetchQuery('/todos?status=pending') 1155 | refetchQuery('/todos/5') 1156 | ``` 1157 | 1158 | #### Function Query Key Serializer Example 1159 | 1160 | The example below shows how to you build your own functional serializer and use it with React Query: 1161 | 1162 | ```js 1163 | import { ReactQueryConfigProvider, stableStringify } from 'react-query' 1164 | 1165 | // A map to keep track of our function pointers 1166 | const functionSerializerMap = new Map() 1167 | 1168 | function functionQueryKeySerializer(queryKey) { 1169 | if (!queryKey) { 1170 | return [] 1171 | } 1172 | 1173 | let queryFn = queryKey 1174 | let variables 1175 | 1176 | if (Array.isArray(queryKey)) { 1177 | queryFn = queryKey[0] 1178 | variables = queryKey[1] 1179 | } 1180 | 1181 | // Get or create an ID for the function pointer 1182 | const queryGroupId = 1183 | functionSerializerMap.get(queryFn) || 1184 | (() => { 1185 | const id = Date.now() 1186 | functionSerializerMap.set(queryFn, id) 1187 | return id 1188 | })() 1189 | 1190 | const variablesIsObject = isObject(variables) 1191 | 1192 | const variablesHash = variablesIsObject ? stableStringify(variables) : '' 1193 | 1194 | const queryHash = `${queryGroupId}_${variablesHash}` 1195 | 1196 | return [queryHash, queryGroupId, variablesHash, variables] 1197 | } 1198 | 1199 | const queryConfig = { 1200 | queryKeySerializerFn: functionQueryKeySerializer, 1201 | } 1202 | 1203 | function App() { 1204 | return ( 1205 | 1206 | ... 1207 | 1208 | ) 1209 | } 1210 | // Heck, you can even make your own custom useQueryHook! 1211 | 1212 | function useFunctionQuery(functionTuple, options) { 1213 | const [queryFn, variables] = Array.isArray(functionTuple) 1214 | ? functionTuple 1215 | : [functionTuple] 1216 | return useQuery(functionTuple, queryFn, options) 1217 | } 1218 | 1219 | // Use it in your app! 1220 | 1221 | function Todos() { 1222 | const todosQuery = useFunctionQuery(getTodos) 1223 | } 1224 | 1225 | function FilteredTodos({ status = 'pending' }) { 1226 | const todosQuery = useFunctionQuery([getTodos, { status }]) 1227 | } 1228 | 1229 | function Todo({ id }) { 1230 | const todoQuery = useFunctionQuery([getTodo, { id }]) 1231 | } 1232 | 1233 | refetchQuery(getTodos) 1234 | refetchQuery([getTodos, { status: 'pending' }]) 1235 | refetchQuery([getTodo, { id: 5 }]) 1236 | ``` 1237 | 1238 | # API 1239 | 1240 | ## `useQuery` 1241 | 1242 | ```js 1243 | const { 1244 | data, 1245 | error, 1246 | isFetching, 1247 | isCached, 1248 | failureCount, 1249 | isLoading, 1250 | refetch, 1251 | // with paginated mode enabled 1252 | isFetchingMore, 1253 | canFetchMore, 1254 | fetchMore, 1255 | } = useQuery(queryKey, queryFn, { 1256 | manual, 1257 | paginated, 1258 | getCanFetchMore, 1259 | retry, 1260 | retryDelay, 1261 | staleTime 1262 | cacheTime, 1263 | refetchInterval, 1264 | refetchIntervalInBackground, 1265 | refetchOnWindowFocus, 1266 | onSuccess, 1267 | onError, 1268 | suspense, 1269 | initialData 1270 | }) 1271 | ``` 1272 | 1273 | ### Options 1274 | 1275 | - `queryKey: String | [String, Variables: Object] | falsey | Function => queryKey` 1276 | - **Required** 1277 | - The query key to use for this query. 1278 | - If a string is passed, it will be used as the query key. 1279 | - If a `[String, Object]` tuple is passed, they will be serialized into a stable query key. See [Query Keys](#query-keys) for more information. 1280 | - If a falsey value is passed, the query will be disabled and not run automatically. 1281 | - If a function is passed, it should resolve to any other valid query key type. If the function throws, the query will be disabled and not run automatically. 1282 | - The query will automatically update when this key changes (if the key is not falsey and if `manual` is not set to `true`). 1283 | - `Variables: Object` 1284 | - If a tuple with variables is passed, this object should be **serializable**. 1285 | - Nested arrays and objects are supported. 1286 | - The order of object keys is sorted to be stable before being serialized into the query key. 1287 | - `queryFn: Function(variables) => Promise(data/error)` 1288 | - **Required** 1289 | - The function that the query will use to request data. 1290 | - Optionally receives the `variables` object passed from either the query key tuple (`useQuery(['todos', variables], queryFn)`) or the `refetch` method's `variables` option, e.g. `refetch({ variables })`. 1291 | - Must return a promise that will either resolves data or throws an error. 1292 | - `paginated: Boolean` 1293 | - Set this to `true` to enable `paginated` mode. 1294 | - In this mode, new pagination utilities are returned from `useQuery` and `data` becomes an array of page results. 1295 | - `manual: Boolean` 1296 | - Set this to `true` to disable automatic refetching when the query mounts or changes query keys. 1297 | - To refetch the query, use the `refetch` method returned from the `useQuery` instance. 1298 | - `getCanFetchMore: Function(lastPage, allPages) => Boolean` 1299 | - **Required if using `paginated` mode** 1300 | - When using `paginated` mode, this function should return `true` if there is more data that can be fetched. 1301 | - `retry: Boolean | Int` 1302 | - If `false`, failed queries will not retry by default. 1303 | - If `true`, failed queries will retry infinitely. 1304 | - If set to an `Int`, e.g. `3`, failed queries will retry until the failed query count meets that number. 1305 | - `retryDelay: Function(retryAttempt: Int) => Int` 1306 | - This function receives a `retryAttempt` integer and returns the delay to apply before the next attempt in milliseconds. 1307 | - A function like `attempt => Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)` applies exponential backoff. 1308 | - A function like `attempt => attempt * 1000` applies linear backoff. 1309 | - `staleTime: Int` 1310 | - The time in milliseconds that cache data remains fresh. After a successful cache update, that cache data will become stale after this duration. 1311 | - `cacheTime: Int` 1312 | - The time in milliseconds that unused/inactive cache data remains in memory. When a query's cache becomes unused or inactive, that cache data will be garbage collected after this duration. 1313 | - `refetchInterval: false | Integer` 1314 | - Optional 1315 | - If set to a number, all queries will continuously refetch at this frequency in milliseconds 1316 | - `refetchIntervalInBackground: Boolean` 1317 | - Optional 1318 | - If set to `true`, queries that are set to continuously refetch with a `refetchInterval` will continue to refetch while their tab/window is in the background 1319 | - `refetchOnWindowFocus: Boolean` 1320 | - Optional 1321 | - Set this to `false` to disable automatic refetching on window focus (useful, when `refetchAllOnWindowFocus` is set to `true`). 1322 | - Set this to `true` to enable automatic refetching on window focus (useful, when `refetchAllOnWindowFocus` is set to `false`. 1323 | - `onError: Function(err) => void` 1324 | - Optional 1325 | - This function will fire if the query encounters an error (after all retries have happened) and will be passed the error. 1326 | - `onSuccess: Function(data) => data` 1327 | - Optional 1328 | - This function will fire any time the query successfully fetches new data. 1329 | - `suspense: Boolean` 1330 | - Optional 1331 | - Set this to `true` to enable suspense mode. 1332 | - When `true`, `useQuery` will suspend when `isLoading` would normally be `true` 1333 | - When `true`, `useQuery` will throw runtime errors when `error` would normally be truthy 1334 | - `initialData: any` 1335 | - Optional 1336 | - If set, this value will be used as the initial data for the query (as long as the query hasn't been created or cached yet) 1337 | 1338 | ### Returns 1339 | 1340 | - `data: null | Any` 1341 | - Defaults to `null`. 1342 | - The last successfully resolved data for the query. 1343 | - `error: null | Error` 1344 | - The error object for the query, if an error was thrown. 1345 | - `isLoading: Boolean` 1346 | - Will be `true` if the query is both fetching and does not have any cached data to display. 1347 | - `isFetching: Boolean` 1348 | - Will be `true` if the query is currently fetching, including background fetching. 1349 | - `isCached: Boolean` 1350 | - Will be `true` if the query's response is currently cached. 1351 | - `failureCount: Integer` 1352 | - The failure count for the query. 1353 | - Incremented every time the query fails. 1354 | - Reset to `0` when the query succeeds. 1355 | - `refetch: Function({ variables: Object, merge: Function, disableThrow: Boolean })` 1356 | - A function to manually refetch the query. 1357 | - Supports custom variables (useful for "fetch more" calls). 1358 | - Supports custom data merging (useful for "fetch more" calls). 1359 | - Set `disableThrow` to true to disable this function from throwing if an error is encountered. 1360 | - `isFetchingMore: Boolean` 1361 | - If using `paginated` mode, this will be `true` when fetching more results using the `fetchMore` function. 1362 | - `canFetchMore: Boolean` 1363 | - If using `paginated` mode, this will be `true` if there is more data to be fetched (known via the required `getCanFetchMore` option function). 1364 | - `fetchMore: Function(variables) => Promise` 1365 | - If using `paginated` mode, this function allows you to fetch the next "page" of results. 1366 | - `variables` should be an object that is passed to your query function to retrieve the next page of results. 1367 | 1368 | ## `useMutation` 1369 | 1370 | ```js 1371 | const [mutate, { data, isLoading, error }] = useMutation(mutationFn, { 1372 | refetchQueries, 1373 | refetchQueriesOnFailure, 1374 | }) 1375 | 1376 | const promise = mutate(variables, { updateQuery, waitForRefetchQueries }) 1377 | ``` 1378 | 1379 | ### Options 1380 | 1381 | - `mutationFn: Function(variables) => Promise` 1382 | - **Required** 1383 | - A function that performs an asynchronous task and returns a promise. 1384 | - `refetchQueries: Array` 1385 | - Optional 1386 | - When the mutation succeeds, these queries will be automatically refetched. 1387 | - Must be an array of query keys, e.g. `['todos', ['todo', { id: 5 }], 'reminders']`. 1388 | - `refetchQueriesOnFailure: Boolean` 1389 | - Defaults to `false` 1390 | - Set this to `true` if you want `refetchQueries` to be refetched regardless of the mutation succeeding. 1391 | - `variables: any` 1392 | - Optional 1393 | - The variables object to pass to the `mutationFn`. 1394 | - `updateQuery: QueryKey` 1395 | - Optional 1396 | - The query key for the individual query to update with the response from this mutation. 1397 | - Suggested use is for `update` mutations that regularly return the updated data with the mutation. This saves you from making another unnecessary network call to refetch the data. 1398 | - `waitForRefetchQueries: Boolean` 1399 | - Optional 1400 | - If set to `true`, the promise returned by `mutate()` will not resolve until refetched queries are resolved as well. 1401 | 1402 | ### Returns 1403 | 1404 | - `mutate: Function(variables, { updateQuery })` 1405 | - The mutation function you can call with variables to trigger the mutation and optionally update a query with its response. 1406 | - `data: null | Any` 1407 | - Defaults to `null` 1408 | - The last successfully resolved data for the query. 1409 | - `error: null | Error` 1410 | - The error object for the query, if an error was thrown. 1411 | - `isLoading: Boolean` 1412 | - Will be `true` if the query is both fetching and does not have any cached data. 1413 | - `promise: Promise` 1414 | - The promise that is returned by the `mutationFn`. 1415 | 1416 | ## `setQueryData` 1417 | 1418 | `setQueryData` is a function for imperatively updating the response of a query. By default, this function also triggers a background refetch to ensure that the data is eventually consistent with the remote source, but this can be disabled. 1419 | 1420 | ```js 1421 | import { setQueryData } from 'react-query' 1422 | 1423 | const maybePromise = setQueryData(queryKey, data, { shouldRefetch }) 1424 | ``` 1425 | 1426 | ### Options 1427 | 1428 | - `queryKey: QueryKey` 1429 | - **Required** 1430 | - The query key for the individual query to update with new data. 1431 | - `data: any | Function(old) => any` 1432 | - **Required** 1433 | - Must either be the new data or a function that receives the old data and returns the new data. 1434 | - `shouldRefetch: Boolean` 1435 | - Optional 1436 | - Defaults to `true` 1437 | - Set this to `false` to disable the automatic background refetch from happening. 1438 | 1439 | ### Returns 1440 | 1441 | - `maybePromise: undefined | Promise` 1442 | - If `shouldRefetch` is `true`, a promise is returned that will either resolve when the query refetch is complete or will reject if the refetch fails (after its respective retry configurations is done). 1443 | 1444 | ## `refetchQuery` 1445 | 1446 | `refetchQuery` is a function that can be used to trigger a refetch of: 1447 | 1448 | - A group of active queries. 1449 | - A single, specific query. 1450 | 1451 | By default, `refetchQuery` will only refetch stale queries, but the `force` option can be used to include non-stale ones. 1452 | 1453 | ```js 1454 | import { refetchQuery } from 'react-query' 1455 | 1456 | const promise = refetchQuery(queryKey, { force }) 1457 | ``` 1458 | 1459 | ### Options 1460 | 1461 | - `queryKey: QueryKey` 1462 | - **Required** 1463 | - The query key for the query or query group to refetch. 1464 | - If a single `string` is passed, any queries using that `string` or any tuple key queries that include that `string` (e.g. passing `todos` would refetch both `todos` and `['todos', { status: 'done' }]`). 1465 | - If a tuple key is passed, only the exact query with that key will be refetched (e.g. `['todos', { status: 'done' }]` will only refetch queries with that exact key). 1466 | - If a tuple key is passed with the `variables` slot set to `false`, then only queries that match the `string` key and have no variables will be refetched (e.g. `['todos', false]` would only refetch `todos` and not `['todos', { status: 'done' }]`). 1467 | - `force: Boolean` 1468 | - Optional 1469 | - Set this to `true` to force all queries to refetch instead of only stale ones. 1470 | 1471 | ### Returns 1472 | 1473 | - `promise: Promise` 1474 | - A promise is returned that will either resolve when all refetch queries are complete or will reject if any refetch queries fail (after their respective retry configurations are done). 1475 | 1476 | ## `refetchAllQueries` 1477 | 1478 | `refetchAllQueries` is a function for imperatively triggering a refetch of all queries. By default, it will only refetch stale queries, but the `force` option can be used to refetch all queries, including non-stale ones. 1479 | 1480 | ```js 1481 | import { refetchAllQueries } from 'react-query' 1482 | 1483 | const promise = refetchAllQueries({ force, includeInactive }) 1484 | ``` 1485 | 1486 | ### Options 1487 | 1488 | - `force: Boolean` 1489 | - Optional 1490 | - Set this to `true` to force all queries to refetch instead of only stale ones. 1491 | - `includeInactive: Boolean` 1492 | - Optional 1493 | - Set this to `true` to also refetch inactive queries. 1494 | - Overrides the `force` option to be `true`, regardless of it's value. 1495 | 1496 | ### Returns 1497 | 1498 | - `promise: Promise` 1499 | - A promise is returned that will either resolve when all refetch queries are complete or will reject if any refetch queries fail (after their respective retry configurations are done). 1500 | 1501 | ## `useIsFetching` 1502 | 1503 | `useIsFetching` is an optional hook that returns `true` if any query in your application is loading or fetching in the background (useful for app-wide loading indicators). 1504 | 1505 | ```js 1506 | import { useIsFetching } from 'react-query' 1507 | 1508 | const isFetching = useIsFetching() 1509 | ``` 1510 | 1511 | ### Returns 1512 | 1513 | - `isFetching: Boolean` 1514 | - Will be `true` if any query in your application is loading or fetching in the background. 1515 | 1516 | ## `prefetchQuery` 1517 | 1518 | `prefetchQuery` is a function that can be used to fetch and cache a query response for later before it is needed or rendered with `useQuery`. **Please note** that `prefetch` will not trigger a query fetch if the query is already cached. If you wish, you can force a prefetch for non-stale queries by using the `force` option: 1519 | 1520 | ```js 1521 | import { prefetchQuery } from 'react-query' 1522 | 1523 | const data = await prefetchQuery(queryKey, queryFn, { force, ...config }) 1524 | ``` 1525 | 1526 | ### Options 1527 | 1528 | The options for `prefetchQuery` are exactly the same as those of [`useQuery`](#usequery), with the exception of a `force` option: 1529 | 1530 | - `force: Boolean` 1531 | - Optional 1532 | - Set this to `true` to prefetch a query **even if it is stale**. 1533 | 1534 | ### Returns 1535 | 1536 | - `promise: Promise` 1537 | - A promise is returned that will either resolve with the **query's response data**, or throw with an **error**. 1538 | 1539 | ## `clearQueryCache` 1540 | 1541 | `clearQueryCache` does exactly what it sounds like, it clears all query caches. It does this by: 1542 | 1543 | - Immediately deleting any queries that do not have active subscriptions. 1544 | - Immediately setting `data` to `null` for all queries with active subscriptions. 1545 | 1546 | ```js 1547 | import { clearQueryCache } from 'react-query' 1548 | 1549 | clearQueryCache() 1550 | ``` 1551 | 1552 | ## `ReactQueryConfigProvider` 1553 | 1554 | `ReactQueryConfigProvider` is an optional provider component and can be used to define defaults for all instances of `useQuery` within it's sub-tree: 1555 | 1556 | ```js 1557 | import { ReactQueryConfigProvider } from 'react-query' 1558 | 1559 | const queryConfig = { 1560 | retry: 3, 1561 | retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), 1562 | staleTime: 0, 1563 | cacheTime: 5 * 60 * 1000, 1564 | refetchAllOnWindowFocus: true, 1565 | refetchInterval: false, 1566 | suspense: false, 1567 | } 1568 | 1569 | function App() { 1570 | return ( 1571 | 1572 | ... 1573 | 1574 | ) 1575 | } 1576 | ``` 1577 | 1578 | ### Options 1579 | 1580 | - `config: Object` 1581 | - Must be **stable** or **memoized**. Do not create an inline object! 1582 | - For a description of all config options, please see the [`useQuery` hook](#usequery). 1583 | -------------------------------------------------------------------------------- /examples/auto-refetching/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example: 4 | 5 | - `npm install` or `yarn` 6 | - `npm run dev` or `yarn dev` 7 | -------------------------------------------------------------------------------- /examples/auto-refetching/components/button.js: -------------------------------------------------------------------------------- 1 | export default ({ children, ...props }) => { 2 | return
16 | } -------------------------------------------------------------------------------- /examples/auto-refetching/libs/fetch.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch' 2 | 3 | export default async function (...args) { 4 | const res = await fetch(...args) 5 | return await res.json() 6 | } 7 | -------------------------------------------------------------------------------- /examples/auto-refetching/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "refetch-interval", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "isomorphic-unfetch": "3.0.0", 8 | "next": "9.1.1", 9 | "react-query": "latest" 10 | }, 11 | "scripts": { 12 | "dev": "next", 13 | "start": "next start", 14 | "build": "next build" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/auto-refetching/pages/api/data.js: -------------------------------------------------------------------------------- 1 | // an simple endpoint for getting current list 2 | let list = ['Item 1', 'Item 2', 'Item 3'] 3 | 4 | export default (req, res) => { 5 | if (req.query.add) { 6 | if (!list.includes(req.query.add)) { 7 | list.push(req.query.add) 8 | } 9 | } else if (req.query.clear) { 10 | list = [] 11 | } 12 | res.json(list) 13 | } 14 | -------------------------------------------------------------------------------- /examples/auto-refetching/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button from '../components/button' 3 | import fetch from '../libs/fetch' 4 | 5 | import { useQuery, useMutation } from 'react-query' 6 | 7 | export default () => { 8 | const [value, setValue] = React.useState('') 9 | 10 | const { data, isLoading } = useQuery('todos', () => fetch('/api/data'), { 11 | // Refetch the data every second 12 | refetchInterval: 1000, 13 | }) 14 | 15 | const [mutateAddTodo] = useMutation( 16 | value => fetch(`/api/data?add=${value}`), 17 | { 18 | refetchQueries: ['todos'], 19 | } 20 | ) 21 | 22 | const [mutateClear] = useMutation(value => fetch(`/api/data?clear=1`), { 23 | refetchQueries: ['todos'], 24 | }) 25 | 26 | if (isLoading) return

Loading...

27 | 28 | return ( 29 |
30 |

Auto Refetch with stale-time set to 1s)

31 |

Todo List

32 |
{ 34 | ev.preventDefault() 35 | try { 36 | await mutateAddTodo(value) 37 | setValue('') 38 | } catch {} 39 | }} 40 | > 41 | setValue(ev.target.value)} 45 | /> 46 |
47 | 52 | 53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example: 4 | 5 | - `npm install` or `yarn` 6 | - `npm run dev` or `yarn dev` 7 | -------------------------------------------------------------------------------- /examples/basic/libs/fetch.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch' 2 | 3 | export default async function (...args) { 4 | const res = await fetch(...args) 5 | return await res.json() 6 | } 7 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "isomorphic-unfetch": "3.0.0", 8 | "next": "9.1.1", 9 | "react-query": "latest" 10 | }, 11 | "scripts": { 12 | "dev": "next", 13 | "start": "next start", 14 | "build": "next build" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/basic/pages/[user]/[repo].js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import fetch from '../../libs/fetch' 4 | 5 | import { useQuery } from 'react-query' 6 | 7 | export default () => { 8 | const id = 9 | typeof window !== 'undefined' ? window.location.pathname.slice(1) : '' 10 | 11 | const { data, isLoading, isFetching } = useQuery(['repository', { id }], () => 12 | fetch('/api/data?id=' + id) 13 | ) 14 | 15 | return ( 16 |
17 |

{id}

18 | {isLoading ? ( 19 | 'Loading...' 20 | ) : data ? ( 21 | <> 22 |
23 |

forks: {data.forks_count}

24 |

stars: {data.stargazers_count}

25 |

watchers: {data.watchers}

26 |
27 |
{isFetching ? 'Background Updating...' : ' '}
28 | 29 | ) : null} 30 |
31 |
32 | 33 | Back 34 | 35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /examples/basic/pages/api/data.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch' 2 | 3 | const projects = [ 4 | 'facebook/flipper', 'vuejs/vuepress', 'rust-lang/rust', 'zeit/next.js' 5 | ] 6 | 7 | export default (req, res) => { 8 | if (req.query.id) { 9 | // a slow endpoint for getting repo data 10 | fetch(`https://api.github.com/repos/${req.query.id}`) 11 | .then(resp => resp.json()) 12 | .then(data => { 13 | setTimeout(() => { 14 | res.json(data) 15 | }, 2000) 16 | }) 17 | 18 | return 19 | } 20 | setTimeout(() => { 21 | res.json(projects) 22 | }, 2000) 23 | } 24 | -------------------------------------------------------------------------------- /examples/basic/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import fetch from '../libs/fetch' 4 | 5 | import { useQuery, useIsFetching } from 'react-query' 6 | 7 | export default () => { 8 | const { data, isLoading, isFetching } = useQuery('projects', () => 9 | fetch('/api/data') 10 | ) 11 | 12 | return ( 13 |
14 |

Trending Projects

15 |
16 | {isLoading ? ( 17 | 'Loading...' 18 | ) : data ? ( 19 | <> 20 |
21 | {data.map(project => ( 22 |

23 | 24 | {project} 25 | 26 |

27 | ))} 28 |
29 |
{isFetching ? 'Background Updating...' : ' '}
30 | 31 | ) : null} 32 |
33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /examples/custom-hooks/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example: 4 | 5 | - `npm install` or `yarn` 6 | - `npm run dev` or `yarn dev` 7 | -------------------------------------------------------------------------------- /examples/custom-hooks/hooks/use-projects.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'react-query' 2 | 3 | import fetch from '../libs/fetch' 4 | 5 | function useProjects() { 6 | return useQuery('projects', () => fetch('/api/data')) 7 | } 8 | 9 | export default useProjects 10 | -------------------------------------------------------------------------------- /examples/custom-hooks/hooks/use-repository.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'react-query' 2 | 3 | import fetch from '../libs/fetch' 4 | 5 | function useRepository(id) { 6 | return useQuery(['repository', { id }], () => fetch('/api/data?id=' + id)) 7 | } 8 | 9 | export default useRepository 10 | -------------------------------------------------------------------------------- /examples/custom-hooks/libs/fetch.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch' 2 | 3 | export default async function(...args) { 4 | const res = await fetch(...args) 5 | return await res.json() 6 | } 7 | -------------------------------------------------------------------------------- /examples/custom-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "isomorphic-unfetch": "3.0.0", 8 | "next": "9.1.2", 9 | "react-query": "latest" 10 | }, 11 | "scripts": { 12 | "dev": "next", 13 | "start": "next start", 14 | "build": "next build" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/custom-hooks/pages/[user]/[repo].js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import useRepository from '../../hooks/use-repository' 4 | 5 | export default () => { 6 | const id = 7 | typeof window !== 'undefined' ? window.location.pathname.slice(1) : '' 8 | const { data, isLoading, isFetching } = useRepository(id) 9 | 10 | return ( 11 |
12 |

{id}

13 | {isLoading ? ( 14 | 'Loading...' 15 | ) : data ? ( 16 | <> 17 |
18 |

forks: {data.forks_count}

19 |

stars: {data.stargazers_count}

20 |

watchers: {data.watchers}

21 |
22 |
{isFetching ? 'Background Updating...' : ' '}
23 | 24 | ) : null} 25 |
26 |
27 | 28 | Back 29 | 30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /examples/custom-hooks/pages/api/data.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch' 2 | 3 | const projects = [ 4 | 'facebook/flipper', 5 | 'vuejs/vuepress', 6 | 'rust-lang/rust', 7 | 'zeit/next.js', 8 | ] 9 | 10 | export default (req, res) => { 11 | if (req.query.id) { 12 | // a slow endpoint for getting repo data 13 | fetch(`https://api.github.com/repos/${req.query.id}`) 14 | .then(resp => resp.json()) 15 | .then(data => { 16 | setTimeout(() => { 17 | res.json(data) 18 | }, 2000) 19 | }) 20 | 21 | return 22 | } 23 | setTimeout(() => { 24 | res.json(projects) 25 | }, 2000) 26 | } 27 | -------------------------------------------------------------------------------- /examples/custom-hooks/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import useProjects from '../hooks/use-projects' 4 | 5 | export default () => { 6 | const { data, isLoading, isFetching } = useProjects() 7 | 8 | return ( 9 |
10 |

Trending Projects

11 |
12 | {isLoading ? ( 13 | 'Loading...' 14 | ) : data ? ( 15 | <> 16 |
17 | {data.map(project => ( 18 |

19 | 20 | {project} 21 | 22 |

23 | ))} 24 |
25 |
{isFetching ? 'Background Updating...' : ' '}
26 | 27 | ) : null} 28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /examples/focus-refetching/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example: 4 | 5 | - `npm install` or `yarn` 6 | - `npm run dev` or `yarn dev` 7 | -------------------------------------------------------------------------------- /examples/focus-refetching/components/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ children, ...props }) => { 4 | return ( 5 |
6 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /examples/focus-refetching/libs/auth.js: -------------------------------------------------------------------------------- 1 | // mock login and logout 2 | 3 | export function login() { 4 | document.cookie = 'swr-test-token=swr;' 5 | } 6 | 7 | export function logout() { 8 | document.cookie = 'swr-test-token=; expires=Thu, 01 Jan 1970 00:00:01 GMT;' 9 | } 10 | -------------------------------------------------------------------------------- /examples/focus-refetching/libs/fetch.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch' 2 | 3 | export default async function (...args) { 4 | const res = await fetch(...args) 5 | return await res.json() 6 | } 7 | -------------------------------------------------------------------------------- /examples/focus-refetching/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "focus-revalidate", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "isomorphic-unfetch": "3.0.0", 8 | "next": "9.1.1", 9 | "react-query": "latest" 10 | }, 11 | "scripts": { 12 | "dev": "next", 13 | "start": "next start", 14 | "build": "next build" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/focus-refetching/pages/api/user.js: -------------------------------------------------------------------------------- 1 | // an endpoint for getting user info 2 | export default (req, res) => { 3 | if (req.cookies['swr-test-token'] === 'swr') { 4 | // authorized 5 | res.json({ 6 | loggedIn: true, 7 | name: 'Tanner', 8 | avatar: 'https://github.com/tannerlinsley.png', 9 | }) 10 | return 11 | } 12 | 13 | res.json({ 14 | loggedIn: false, 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /examples/focus-refetching/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button from '../components/button' 3 | import fetch from '../libs/fetch' 4 | import { login, logout } from '../libs/auth' 5 | 6 | import { useQuery, useMutation } from 'react-query' 7 | 8 | export default () => { 9 | const { data, isLoading } = useQuery('user', () => fetch('/api/user')) 10 | 11 | const [logoutMutation] = useMutation(logout, { 12 | refetchQueries: ['user'], 13 | }) 14 | 15 | const [loginMutation] = useMutation(login, { 16 | refetchQueries: ['user'], 17 | }) 18 | 19 | if (isLoading) return

Loading...

20 | if (data && data.loggedIn) { 21 | return ( 22 |
23 |

Welcome, {data.name}

24 | 25 | 32 |
33 | ) 34 | } else { 35 | return ( 36 |
37 |

Please login

38 | 45 |
46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/load-more-pagination/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example: 4 | 5 | - `npm install` or `yarn` 6 | - `npm run dev` or `yarn dev` 7 | -------------------------------------------------------------------------------- /examples/load-more-pagination/libs/fetch.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch' 2 | 3 | export default async function (...args) { 4 | const res = await fetch(...args) 5 | return await res.json() 6 | } 7 | -------------------------------------------------------------------------------- /examples/load-more-pagination/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pagination", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "isomorphic-unfetch": "3.0.0", 8 | "next": "9.1.1", 9 | "react-query": "latest" 10 | }, 11 | "scripts": { 12 | "dev": "next", 13 | "start": "next start", 14 | "build": "next build" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/load-more-pagination/pages/about.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | export default () => { 4 | return back 5 | } -------------------------------------------------------------------------------- /examples/load-more-pagination/pages/api/projects.js: -------------------------------------------------------------------------------- 1 | // an endpoint for getting projects data 2 | export default (req, res) => { 3 | const cursor = parseInt(req.query.cursor || 0) 4 | 5 | const data = Array(3) 6 | .fill(0) 7 | .map((_, i) => { 8 | return { 9 | name: 'Project ' + (i + cursor) + ` (server time: ${Date.now()})`, 10 | id: i + cursor, 11 | } 12 | }) 13 | 14 | const nextId = cursor < 9 ? data[data.length - 1].id + 1 : null 15 | 16 | setTimeout(() => res.json({ data, nextId }), 1000) 17 | } 18 | -------------------------------------------------------------------------------- /examples/load-more-pagination/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import fetch from '../libs/fetch' 3 | import Link from 'next/link' 4 | 5 | import { useQuery } from 'react-query' 6 | 7 | export default () => { 8 | const { 9 | data, 10 | isLoading, 11 | isFetching, 12 | isFetchingMore, 13 | fetchMore, 14 | canFetchMore, 15 | } = useQuery( 16 | 'projects', 17 | ({ nextId } = {}) => fetch('/api/projects?cursor=' + (nextId || 0)), 18 | { 19 | paginated: true, 20 | getCanFetchMore: lastPage => lastPage.nextId, 21 | } 22 | ) 23 | 24 | const loadMore = async () => { 25 | try { 26 | const { nextId } = data[data.length - 1] 27 | 28 | await fetchMore({ 29 | nextId, 30 | }) 31 | } catch {} 32 | } 33 | 34 | return ( 35 |
36 |

Pagination

37 | {isLoading ? ( 38 |

Loading...

39 | ) : data ? ( 40 | <> 41 | {data.map((page, i) => ( 42 | 43 | {page.data.map(project => ( 44 |

52 | {project.name} 53 |

54 | ))} 55 |
56 | ))} 57 |
58 | {canFetchMore ? ( 59 | 62 | ) : ( 63 | 'Nothing more to fetch.' 64 | )} 65 |
66 |
67 | {isFetching && !isFetchingMore ? 'Background Updating...' : null} 68 |
69 | 70 | ) : null} 71 |
72 | 73 | Go to another page 74 | 75 |
76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /examples/optimistic-updates/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example: 4 | 5 | - `npm install` or `yarn` 6 | - `npm run dev` or `yarn dev` 7 | -------------------------------------------------------------------------------- /examples/optimistic-updates/libs/fetch.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch' 2 | 3 | export default async function (...args) { 4 | const res = await fetch(...args) 5 | return await res.json() 6 | } 7 | -------------------------------------------------------------------------------- /examples/optimistic-updates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "isomorphic-unfetch": "3.0.0", 8 | "next": "9.1.1", 9 | "react-query": "latest" 10 | }, 11 | "scripts": { 12 | "dev": "next", 13 | "start": "next start", 14 | "build": "next build" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/optimistic-updates/pages/api/data.js: -------------------------------------------------------------------------------- 1 | const data = [] 2 | 3 | function shouldFail() { 4 | return Math.random() > 0.7 5 | } 6 | 7 | export default (req, res) => { 8 | if (req.method === 'POST') { 9 | const body = JSON.parse(req.body) 10 | 11 | // sometimes it will fail, this will cause a regression on the UI 12 | 13 | if (shouldFail()) { 14 | throw new Error('Could not add item!') 15 | } 16 | 17 | data.push(body.text.toUpperCase()) 18 | res.json(data) 19 | return 20 | } 21 | 22 | setTimeout(() => { 23 | res.json(data) 24 | }, 300) 25 | } 26 | -------------------------------------------------------------------------------- /examples/optimistic-updates/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import fetch from '../libs/fetch' 3 | 4 | import { useQuery, useMutation, setQueryData } from 'react-query' 5 | 6 | export default () => { 7 | const [text, setText] = React.useState('') 8 | const { data, isLoading, isFetching } = useQuery('todos', () => 9 | fetch('/api/data') 10 | ) 11 | 12 | const [mutatePostTodo] = useMutation( 13 | text => 14 | fetch('/api/data', { 15 | method: 'POST', 16 | body: JSON.stringify({ text }), 17 | }), 18 | { 19 | refetchQueries: ['todos'], 20 | // to revalidate the data and ensure the UI doesn't 21 | // remain in an incorrect state, ALWAYS trigger a 22 | // a refetch of the data, even on failure 23 | refetchQueriesOnFailure: true, 24 | } 25 | ) 26 | 27 | async function handleSubmit(event) { 28 | event.preventDefault() 29 | // mutate current data to optimistically update the UI 30 | // the fetch below could fail, so we need to revalidate 31 | // regardless 32 | 33 | setQueryData('todos', [...data, text], { 34 | shouldRefetch: false, 35 | }) 36 | 37 | try { 38 | // send text to the API 39 | await mutatePostTodo(text) 40 | setText('') 41 | } catch (err) { 42 | console.error(err) 43 | } 44 | } 45 | 46 | return ( 47 |
48 |
49 | setText(event.target.value)} 52 | value={text} 53 | /> 54 | 55 |
56 | 66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /examples/sandbox/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"], 3 | "plugins": ["styled-components"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/sandbox/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app", "prettier"], 3 | "rules": { 4 | // "eqeqeq": 0, 5 | // "jsx-a11y/anchor-is-valid": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/sandbox/.gitignore: -------------------------------------------------------------------------------- 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 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/sandbox/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /examples/sandbox/.rescriptsrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const resolveFrom = require('resolve-from') 3 | 4 | const fixLinkedDependencies = config => { 5 | config.resolve = { 6 | ...config.resolve, 7 | alias: { 8 | ...config.resolve.alias, 9 | react$: resolveFrom(path.resolve('node_modules'), 'react'), 10 | 'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'), 11 | }, 12 | } 13 | return config 14 | } 15 | 16 | const includeSrcDirectory = config => { 17 | config.resolve = { 18 | ...config.resolve, 19 | modules: [path.resolve('src'), ...config.resolve.modules], 20 | } 21 | return config 22 | } 23 | 24 | module.exports = [ 25 | ['use-babel-config', '.babelrc'], 26 | ['use-eslint-config', '.eslintrc'], 27 | fixLinkedDependencies, 28 | // includeSrcDirectory, 29 | ] 30 | -------------------------------------------------------------------------------- /examples/sandbox/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example, [Open It In Codesandbox](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/sandbox) 4 | -------------------------------------------------------------------------------- /examples/sandbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "rescripts start", 5 | "build": "rescripts build", 6 | "test": "rescripts test", 7 | "eject": "rescripts eject" 8 | }, 9 | "dependencies": { 10 | "react-query": "latest", 11 | "react": "^16.8.6", 12 | "react-dom": "^16.8.6", 13 | "react-scripts": "3.0.1", 14 | "stop-runaway-react-effects": "^1.2.0", 15 | "styled-components": "^4.3.2" 16 | }, 17 | "devDependencies": { 18 | "@rescripts/cli": "^0.0.11", 19 | "@rescripts/rescript-use-babel-config": "^0.0.8", 20 | "@rescripts/rescript-use-eslint-config": "^0.0.9", 21 | "babel-eslint": "10.0.1" 22 | }, 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/sandbox/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/react-query/07b0a66f39a9ce3b5b61ec720f3aebae6128bce2/examples/sandbox/public/favicon.ico -------------------------------------------------------------------------------- /examples/sandbox/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/sandbox/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/sandbox/src/index.app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import styled from "styled-components"; 4 | 5 | import { 6 | ReactQueryConfigProvider, 7 | useQuery, 8 | useMutation, 9 | refetchAllQueries, 10 | useIsFetching, 11 | queries, 12 | globalStateListeners 13 | } from "react-query"; 14 | 15 | import "./styles.css"; 16 | 17 | const QueryStateList = styled.div` 18 | display: flex; 19 | padding: 0.15rem; 20 | flex-wrap: wrap; 21 | `; 22 | 23 | const QueryState = styled.div` 24 | margin: 0.3rem; 25 | color: white; 26 | padding: 0.3rem; 27 | font-size: 12px; 28 | border-radius: 0.3rem; 29 | font-weight: bold; 30 | text-shadow: 0 0 10px black; 31 | 32 | background: ${props => 33 | props.isFetching 34 | ? "orange" 35 | : props.isInactive 36 | ? "grey" 37 | : props.isStale 38 | ? "red" 39 | : "green"}; 40 | `; 41 | 42 | const QueryKeys = styled.div` 43 | font-size: 0.7rem; 44 | `; 45 | 46 | const QueryKey = styled.span` 47 | width: 20px; 48 | height: 20px; 49 | display: inline-block; 50 | `; 51 | 52 | let id = 0; 53 | let list = [ 54 | "apple", 55 | "banana", 56 | "pineapple", 57 | "grapefruit", 58 | "dragonfruit", 59 | "grapes" 60 | ].map(d => ({ id: id++, name: d, notes: "These are some notes" })); 61 | 62 | let errorRate = 0.05; 63 | let queryTimeMin = 1000; 64 | let queryTimeMax = 2000; 65 | 66 | const fetchTodos = ({ filter } = {}) => { 67 | console.log("fetchTodos", { filter }); 68 | return new Promise((resolve, reject) => { 69 | setTimeout(() => { 70 | if (Math.random() < errorRate) { 71 | return reject( 72 | new Error(JSON.stringify({ fetchTodos: { filter } }, null, 2)) 73 | ); 74 | } 75 | resolve(list.filter(d => d.name.includes(filter))); 76 | }, queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin)); 77 | }); 78 | }; 79 | 80 | const fetchTodoById = ({ id }) => { 81 | console.log("fetchTodoById", { id }); 82 | return new Promise((resolve, reject) => { 83 | setTimeout(() => { 84 | if (Math.random() < errorRate) { 85 | return reject( 86 | new Error(JSON.stringify({ fetchTodoById: { id } }, null, 2)) 87 | ); 88 | } 89 | resolve(list.find(d => d.id === id)); 90 | }, queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin)); 91 | }); 92 | }; 93 | 94 | const postTodo = ({ name, notes }) => { 95 | console.log("postTodo", { name, notes }); 96 | return new Promise((resolve, reject) => { 97 | setTimeout(() => { 98 | if (Math.random() < errorRate) { 99 | return reject( 100 | new Error(JSON.stringify({ postTodo: { name, notes } }, null, 2)) 101 | ); 102 | } 103 | const todo = { name, notes, id: id++ }; 104 | list = [...list, todo]; 105 | resolve(todo); 106 | }, queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin)); 107 | }); 108 | }; 109 | 110 | const patchTodo = todo => { 111 | console.log("patchTodo", todo); 112 | return new Promise((resolve, reject) => { 113 | setTimeout(() => { 114 | if (Math.random() < errorRate) { 115 | return reject(new Error(JSON.stringify({ patchTodo: todo }, null, 2))); 116 | } 117 | list = list.map(d => { 118 | if (d.id === todo.id) { 119 | return todo; 120 | } 121 | return d; 122 | }); 123 | resolve(todo); 124 | }, queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin)); 125 | }); 126 | }; 127 | 128 | export function useQueries() { 129 | const [state, setState] = React.useState({ queries }); 130 | 131 | React.useEffect(() => { 132 | const fn = () => { 133 | setState({ queries }); 134 | }; 135 | 136 | globalStateListeners.push(fn); 137 | 138 | return () => { 139 | globalStateListeners.splice(globalStateListeners.indexOf(fn), 1); 140 | }; 141 | }, []); 142 | 143 | return state.queries; 144 | } 145 | 146 | function Root() { 147 | const [staleTime, setStaleTime] = React.useState(1000); 148 | const [cacheTime, setCacheTime] = React.useState(3000); 149 | const [localErrorRate, setErrorRate] = React.useState(errorRate); 150 | const [localFetchTimeMin, setLocalFetchTimeMin] = React.useState( 151 | queryTimeMin 152 | ); 153 | const [localFetchTimeMax, setLocalFetchTimeMax] = React.useState( 154 | queryTimeMax 155 | ); 156 | 157 | React.useEffect(() => { 158 | errorRate = localErrorRate; 159 | queryTimeMin = localFetchTimeMin; 160 | queryTimeMax = localFetchTimeMax; 161 | }, [localErrorRate, localFetchTimeMax, localFetchTimeMin]); 162 | 163 | const queryConfig = React.useMemo( 164 | () => ({ 165 | staleTime, 166 | cacheTime 167 | }), 168 | [cacheTime, staleTime] 169 | ); 170 | 171 | return ( 172 | 173 |

174 | The "staleTime" and "cacheTime" durations have been altered in this 175 | example to show how query stale-ness and query caching work on a 176 | granular level 177 |

178 |
179 | Stale Time:{" "} 180 | setStaleTime(parseFloat(e.target.value, 10))} 186 | style={{ width: "100px" }} 187 | /> 188 |
189 |
190 | Cache Time:{" "} 191 | setCacheTime(parseFloat(e.target.value, 10))} 197 | style={{ width: "100px" }} 198 | /> 199 |
200 |
201 |
202 | Error Rate:{" "} 203 | setErrorRate(parseFloat(e.target.value, 10))} 210 | style={{ width: "100px" }} 211 | /> 212 |
213 |
214 | Fetch Time Min:{" "} 215 | setLocalFetchTimeMin(parseFloat(e.target.value, 10))} 221 | style={{ width: "60px" }} 222 | />{" "} 223 |
224 |
225 | Fetch Time Max:{" "} 226 | setLocalFetchTimeMax(parseFloat(e.target.value, 10))} 232 | style={{ width: "60px" }} 233 | /> 234 |
235 |
236 | 237 |
238 | ); 239 | } 240 | 241 | function App() { 242 | const [editingIndex, setEditingIndex] = React.useState(null); 243 | const [views, setViews] = React.useState(["", "fruit", "grape"]); 244 | // const [views, setViews] = React.useState([""]); 245 | 246 | const queries = useQueries(); 247 | 248 | return ( 249 |
250 | Queries - Click a query to log to console) 251 | 252 | {queries.map(query => { 253 | const { 254 | queryHash, 255 | state: { isFetching, isStale, isInactive } 256 | } = query; 257 | 258 | return ( 259 | { 265 | console.info(query); 266 | }} 267 | > 268 | {queryHash} 269 | 270 | ); 271 | })} 272 | 273 | 274 | 275 | Cached{" "} 276 | 277 | 278 | Fetching{" "} 279 | 280 | 281 | Stale{" "} 282 | 283 | 284 | Inactive 285 | 286 | 287 |
288 | 289 |
290 | 299 |
300 |
301 |
302 | {views.map((view, index) => ( 303 |
304 | { 308 | setViews(old => [...old, ""]); 309 | }} 310 | /> 311 |
312 |
313 | ))} 314 | 321 |
322 | {editingIndex !== null ? ( 323 | <> 324 | 328 |
329 | 330 | ) : null} 331 | 332 |
333 | ); 334 | } 335 | 336 | function RefreshingBanner() { 337 | const isFetching = useIsFetching(); 338 | return ( 339 |
340 | Global isFetching: {isFetching.toString()} 341 |
342 |
343 |
344 | ); 345 | } 346 | 347 | function Todos({ initialFilter = "", setEditingIndex }) { 348 | const [filter, setFilter] = React.useState(initialFilter); 349 | 350 | const { 351 | data, 352 | isLoading, 353 | isFetching, 354 | error, 355 | failureCount, 356 | refetch 357 | } = useQuery(["todos", { filter }], fetchTodos); 358 | 359 | return ( 360 |
361 |
362 | 366 |
367 | {isLoading ? ( 368 | Loading... (Attempt: {failureCount + 1}) 369 | ) : error ? ( 370 | 371 | Error!{" "} 372 | 373 | 374 | ) : ( 375 | <> 376 | 388 |
389 | {isFetching ? ( 390 | 391 | Background Refreshing... (Attempt: {failureCount + 1}) 392 | 393 | ) : ( 394 |   395 | )} 396 |
397 | 398 | )} 399 |
400 | ); 401 | } 402 | 403 | function EditTodo({ editingIndex, setEditingIndex }) { 404 | // Don't attempt to query until editingIndex is truthy 405 | const { 406 | data, 407 | isLoading, 408 | isFetching, 409 | error, 410 | failureCount, 411 | refetch 412 | } = useQuery( 413 | editingIndex !== null && ["todo", { id: editingIndex }], 414 | fetchTodoById 415 | ); 416 | 417 | const [todo, setTodo] = React.useState(data); 418 | 419 | React.useEffect(() => { 420 | if (editingIndex !== null && data) { 421 | console.log(data); 422 | setTodo(data); 423 | } else { 424 | setTodo(); 425 | } 426 | }, [data, editingIndex]); 427 | 428 | const [mutate, mutationState] = useMutation(patchTodo, { 429 | refetchQueries: ["todos"] 430 | }); 431 | 432 | const onSave = () => { 433 | try { 434 | mutate(todo, { 435 | // Update `todos` and the individual todo queries when this mutation succeeds 436 | updateQuery: ["todo", { id: editingIndex }] 437 | }); 438 | } catch { 439 | // Errors are shown in the UI 440 | } 441 | }; 442 | 443 | const canEditOrSave = isLoading || mutationState.isLoading; 444 | 445 | return ( 446 |
447 |
448 | {data ? ( 449 | <> 450 | Editing 451 | Todo "{data.name}" (# 452 | {editingIndex}) 453 | 454 | ) : null} 455 |
456 | {isLoading ? ( 457 | Loading... (Attempt: {failureCount + 1}) 458 | ) : error ? ( 459 | 460 | Error!{" "} 461 | 462 | 463 | ) : todo ? ( 464 | <> 465 | 476 | 487 |
488 | 491 |
492 |
493 | {mutationState.isLoading 494 | ? "Saving..." 495 | : mutationState.error 496 | ? String(mutationState.error) 497 | : mutationState.data 498 | ? "Saved!" 499 | : null} 500 |
501 |
502 | {isFetching ? ( 503 | 504 | Background Refreshing... (Attempt: {failureCount + 1}) 505 | 506 | ) : ( 507 |   508 | )} 509 |
510 | 511 | ) : null} 512 |
513 | ); 514 | } 515 | 516 | function AddTodo() { 517 | const [name, setName] = React.useState(""); 518 | 519 | const [mutate, { isLoading, error, data }] = useMutation(postTodo, { 520 | refetchQueries: ["todos"] 521 | }); 522 | 523 | return ( 524 |
525 | setName(e.target.value)} 528 | disabled={isLoading} 529 | /> 530 | 542 |
543 | {isLoading 544 | ? "Saving..." 545 | : error 546 | ? String(error) 547 | : data 548 | ? "Saved!" 549 | : null} 550 |
551 |
552 | ); 553 | } 554 | 555 | const rootElement = document.getElementById("root"); 556 | ReactDOM.render(, rootElement); 557 | -------------------------------------------------------------------------------- /examples/sandbox/src/index.js: -------------------------------------------------------------------------------- 1 | import { hijackEffects } from "stop-runaway-react-effects"; 2 | 3 | if (process.env.NODE_ENV !== "production") { 4 | hijackEffects(); 5 | } 6 | 7 | require("./index.app"); 8 | -------------------------------------------------------------------------------- /examples/sandbox/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 1rem; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | color: white; 10 | background: #222; 11 | } 12 | 13 | code { 14 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 15 | monospace; 16 | } 17 | -------------------------------------------------------------------------------- /examples/suspense/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/suspense/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app", "prettier"], 3 | "rules": { 4 | // "eqeqeq": 0, 5 | // "jsx-a11y/anchor-is-valid": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/suspense/.gitignore: -------------------------------------------------------------------------------- 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 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/suspense/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /examples/suspense/.rescriptsrc.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const resolveFrom = require("resolve-from"); 3 | 4 | const fixLinkedDependencies = config => { 5 | config.resolve = { 6 | ...config.resolve, 7 | alias: { 8 | ...config.resolve.alias, 9 | react$: resolveFrom(path.resolve("node_modules"), "react"), 10 | "react-dom$": resolveFrom(path.resolve("node_modules"), "react-dom") 11 | } 12 | }; 13 | return config; 14 | }; 15 | 16 | const includeSrcDirectory = config => { 17 | config.resolve = { 18 | ...config.resolve, 19 | modules: [path.resolve("src"), ...config.resolve.modules] 20 | }; 21 | return config; 22 | }; 23 | 24 | module.exports = [ 25 | ["use-babel-config", ".babelrc"], 26 | ["use-eslint-config", ".eslintrc"], 27 | fixLinkedDependencies 28 | // includeSrcDirectory, 29 | ]; 30 | -------------------------------------------------------------------------------- /examples/suspense/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example, [Open It In Codesandbox](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/sandbox) 4 | -------------------------------------------------------------------------------- /examples/suspense/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "0.0.0-experimental-5faf377df", 9 | "react-dom": "0.0.0-experimental-5faf377df", 10 | "react-query": "latest", 11 | "react-scripts": "3.0.1" 12 | }, 13 | "devDependencies": { 14 | "@rescripts/cli": "^0.0.11", 15 | "@rescripts/rescript-use-babel-config": "^0.0.8", 16 | "@rescripts/rescript-use-eslint-config": "^0.0.9", 17 | "babel-eslint": "10.0.1" 18 | }, 19 | "scripts": { 20 | "start": "rescripts start", 21 | "build": "rescripts build", 22 | "test": "rescripts test", 23 | "eject": "rescripts eject" 24 | }, 25 | "browserslist": [ 26 | ">0.2%", 27 | "not dead", 28 | "not ie <= 11", 29 | "not op_mini all" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /examples/suspense/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/react-query/07b0a66f39a9ce3b5b61ec720f3aebae6128bce2/examples/suspense/public/favicon.ico -------------------------------------------------------------------------------- /examples/suspense/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 26 | React App 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/suspense/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/suspense/src/components/Button.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Spinner from "./Spinner"; 4 | 5 | export default function Button({ children, timeoutMs = 3000, onClick }) { 6 | const [startTransition, isPending] = React.useTransition({ 7 | timeoutMs: timeoutMs 8 | }); 9 | 10 | const handleClick = e => { 11 | startTransition(() => { 12 | onClick(e); 13 | }); 14 | }; 15 | 16 | return ( 17 | <> 18 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /examples/suspense/src/components/ErrorBounderay.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Button from "./Button"; 4 | 5 | export default class ErrorBoundary extends React.Component { 6 | state = { error: null }; 7 | static getDerivedStateFromError(error) { 8 | return { error }; 9 | } 10 | componentDidCatch() { 11 | // log the error to the server 12 | } 13 | tryAgain = () => this.setState({ error: null }); 14 | render() { 15 | return this.state.error ? ( 16 |
17 | There was an error. 18 |
{this.state.error.message}
19 |
20 | ) : ( 21 | this.props.children 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/suspense/src/components/Project.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useQuery } from "react-query"; 3 | 4 | import Button from "./Button"; 5 | import Spinner from "./Spinner"; 6 | 7 | import { fetchProject } from "../queries"; 8 | 9 | export default function Project({ activeProject, setActiveProject }) { 10 | const { data, isFetching } = useQuery( 11 | ["project", { id: activeProject }], 12 | fetchProject 13 | ); 14 | 15 | return ( 16 |
17 | 18 |

19 | {activeProject} {isFetching ? : null} 20 |

21 | {data ? ( 22 |
23 |

forks: {data.forks_count}

24 |

stars: {data.stargazers_count}

25 |

watchers: {data.watchers}

26 |
27 | ) : null} 28 |
29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /examples/suspense/src/components/Projects.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useQuery, prefetchQuery } from "react-query"; 3 | 4 | import Button from "./Button"; 5 | import Spinner from "./Spinner"; 6 | 7 | import { fetchProjects, fetchProject } from "../queries"; 8 | 9 | export default function Projects({ setActiveProject }) { 10 | const { data, isFetching } = useQuery("projects", fetchProjects); 11 | 12 | return ( 13 |
14 |

Projects {isFetching ? : null}

15 | {data.map(project => ( 16 |

17 | {" "} 26 | {project.name} 27 |

28 | ))} 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /examples/suspense/src/components/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Spinner() { 4 | return ( 5 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/suspense/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { lazy } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { ReactQueryConfigProvider, prefetchQuery } from "react-query"; 4 | 5 | import "./styles.css"; 6 | 7 | import { fetchProjects } from "./queries"; 8 | 9 | import ErrorBounderay from "./components/ErrorBounderay"; 10 | import Button from "./components/Button"; 11 | 12 | const Projects = lazy(() => import("./components/Projects")); 13 | const Project = lazy(() => import("./components/Project")); 14 | 15 | const queryConfig = { 16 | suspense: true 17 | }; 18 | 19 | function App() { 20 | const [showProjects, setShowProjects] = React.useState(false); 21 | const [activeProject, setActiveProject] = React.useState(null); 22 | 23 | return ( 24 | 25 | 37 | 38 |
39 | 40 | 41 | Loading projects...}> 42 | {showProjects ? ( 43 | activeProject ? ( 44 | 48 | ) : ( 49 | 50 | ) 51 | ) : null} 52 | 53 | 54 |
55 | ); 56 | } 57 | 58 | const rootElement = document.getElementById("root"); 59 | ReactDOM.createRoot(rootElement).render(); 60 | -------------------------------------------------------------------------------- /examples/suspense/src/queries.js: -------------------------------------------------------------------------------- 1 | export async function fetchProjects() { 2 | return (await fetch( 3 | `https://api.github.com/users/tannerlinsley/repos?sort=updated` 4 | )).json(); 5 | } 6 | 7 | export async function fetchProject({ id }) { 8 | return (await fetch( 9 | `https://api.github.com/repos/tannerlinsley/${id}` 10 | )).json(); 11 | } 12 | -------------------------------------------------------------------------------- /examples/suspense/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /media/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/react-query/07b0a66f39a9ce3b5b61ec720f3aebae6128bce2/media/header.png -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/react-query/07b0a66f39a9ce3b5b61ec720f3aebae6128bce2/media/logo.png -------------------------------------------------------------------------------- /media/logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/react-query/07b0a66f39a9ce3b5b61ec720f3aebae6128bce2/media/logo.sketch -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-query", 3 | "version": "0.3.24", 4 | "description": "Hooks for managing, caching and syncing asynchronous and remote data in React", 5 | "author": "tannerlinsley", 6 | "license": "MIT", 7 | "repository": "tannerlinsley/react-query", 8 | "main": "dist/index.js", 9 | "module": "dist/index.es.js", 10 | "scripts": { 11 | "test": "jest", 12 | "test:watch": "jest .", 13 | "build": "NODE_ENV=production rollup -c", 14 | "now-build": "yarn && cd www && yarn && yarn build", 15 | "start": "rollup -c -w", 16 | "prepare": "yarn build", 17 | "release": "yarn publish", 18 | "releaseNext": "yarn publish --tag next", 19 | "format": "prettier {src,src/**,example/src,example/src/**}/*.{md,js,jsx,tsx} --write" 20 | }, 21 | "peerDependencies": { 22 | "react": "^16.6.3" 23 | }, 24 | "dependencies": { 25 | "@types/react-query": "^0.3.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.7.4", 29 | "@babel/plugin-proposal-class-properties": "^7.4.4", 30 | "@babel/preset-env": "^7.4.5", 31 | "@babel/preset-react": "^7.0.0", 32 | "@svgr/rollup": "^4.3.0", 33 | "babel-core": "7.0.0-bridge.0", 34 | "babel-eslint": "9.x", 35 | "babel-jest": "^24.9.0", 36 | "cross-env": "^5.1.4", 37 | "eslint": "5.x", 38 | "eslint-config-prettier": "^4.3.0", 39 | "eslint-config-react-app": "^4.0.1", 40 | "eslint-config-standard": "^12.0.0", 41 | "eslint-config-standard-react": "^7.0.2", 42 | "eslint-plugin-flowtype": "2.x", 43 | "eslint-plugin-import": "2.x", 44 | "eslint-plugin-jsx-a11y": "6.x", 45 | "eslint-plugin-node": "^9.1.0", 46 | "eslint-plugin-prettier": "^3.1.0", 47 | "eslint-plugin-promise": "^4.1.1", 48 | "eslint-plugin-react": "7.x", 49 | "eslint-plugin-react-hooks": "1.5.0", 50 | "eslint-plugin-standard": "^4.0.0", 51 | "fast-async": "^6.3.8", 52 | "jest": "^24.9.0", 53 | "prettier": "^1.18.2", 54 | "react": "^16.8.6", 55 | "react-dom": "^16.8.6", 56 | "rollup": "^1.12.4", 57 | "rollup-plugin-babel": "^4.3.2", 58 | "rollup-plugin-commonjs": "^10.0.0", 59 | "rollup-plugin-node-resolve": "^5.0.0", 60 | "rollup-plugin-peer-deps-external": "^2.2.0", 61 | "rollup-plugin-size": "^0.2.1", 62 | "rollup-plugin-size-snapshot": "^0.10.0", 63 | "rollup-plugin-terser": "^5.1.2" 64 | }, 65 | "files": [ 66 | "dist" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: false, 6 | singleQuote: true, 7 | trailingComma: "es5", 8 | bracketSpacing: true, 9 | jsxBracketSameLine: false, 10 | arrowParens: "avoid", 11 | endOfLine: "auto" 12 | }; 13 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import external from 'rollup-plugin-peer-deps-external' 4 | import resolve from 'rollup-plugin-node-resolve' 5 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot' 6 | import size from 'rollup-plugin-size'; 7 | import pkg from './package.json' 8 | 9 | export default [ 10 | { 11 | input: 'src/index.js', 12 | output: { 13 | file: pkg.main, 14 | format: 'cjs', 15 | sourcemap: true, 16 | }, 17 | plugins: [external(), babel(), resolve(), commonjs(), 18 | size({publish:true,exclude:pkg.module,filename:'sizes-cjs.json',writeFile:process.env.CI?true:false}), 19 | sizeSnapshot()], 20 | }, 21 | { 22 | input: 'src/index.js', 23 | output: { 24 | file: pkg.module, 25 | format: 'es', 26 | sourcemap: true, 27 | }, 28 | plugins: [external(), babel(), 29 | size({publish:true,exclude:pkg.main,filename:'sizes-es.json',writeFile:process.env.CI?true:false}), 30 | sizeSnapshot()], 31 | }, 32 | ] 33 | -------------------------------------------------------------------------------- /sizes-cjs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "timestamp": 1579297800315, 4 | "files": [ 5 | { 6 | "filename": "index.js", 7 | "size": 6873, 8 | "delta": -4 9 | } 10 | ] 11 | } 12 | ] -------------------------------------------------------------------------------- /sizes-es.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "timestamp": 1579297807616, 4 | "files": [ 5 | { 6 | "filename": "index.es.js", 7 | "size": 6736, 8 | "delta": -3 9 | } 10 | ] 11 | } 12 | ] -------------------------------------------------------------------------------- /src/__tests__/setQueryData-test.js: -------------------------------------------------------------------------------- 1 | import { setQueryData } from '../index' 2 | 3 | test('setQueryData does not crash if query could not be found', () => { 4 | expect(() => 5 | setQueryData(['USER', { userId: 1 }], prevUser => ({ 6 | ...prevUser, 7 | name: 'Edvin', 8 | })) 9 | ).not.toThrow() 10 | }) 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export let queries = [] 4 | const cancelledError = {} 5 | export let globalStateListeners = [] 6 | let uid = 0 7 | const configContext = React.createContext() 8 | const isServer = typeof window === 'undefined' 9 | 10 | let defaultConfig = { 11 | retry: 3, 12 | retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), 13 | staleTime: 0, 14 | cacheTime: 5 * 60 * 1000, 15 | refetchAllOnWindowFocus: true, 16 | refetchInterval: false, 17 | suspense: false, 18 | queryKeySerializerFn: defaultQueryKeySerializerFn, 19 | } 20 | 21 | const onWindowFocus = () => { 22 | const { refetchAllOnWindowFocus } = defaultConfig 23 | 24 | if (isDocumentVisible() && isOnline()) { 25 | refetchAllQueries({ 26 | shouldRefetchQuery: query => { 27 | if (typeof query.config.refetchOnWindowFocus === 'undefined') { 28 | return refetchAllOnWindowFocus 29 | } else { 30 | return query.config.refetchOnWindowFocus 31 | } 32 | }, 33 | }).catch(error => { 34 | console.error(error.message) 35 | }) 36 | } 37 | } 38 | 39 | let removePreviousHandler 40 | 41 | export function setFocusHandler(callback) { 42 | // Unsub the old watcher 43 | if (removePreviousHandler) { 44 | removePreviousHandler() 45 | } 46 | // Sub the new watcher 47 | removePreviousHandler = callback(onWindowFocus) 48 | } 49 | 50 | setFocusHandler(handleFocus => { 51 | // Listen to visibillitychange and focus 52 | if (typeof window !== 'undefined' && window.addEventListener) { 53 | window.addEventListener('visibilitychange', handleFocus, false) 54 | window.addEventListener('focus', handleFocus, false) 55 | 56 | return () => { 57 | // Be sure to unsubscribe if a new handler is set 58 | window.removeEventListener('visibilitychange', handleFocus) 59 | window.removeEventListener('focus', handleFocus) 60 | } 61 | } 62 | }) 63 | 64 | export function ReactQueryConfigProvider({ config, children }) { 65 | let configContextValue = React.useContext(configContext) 66 | 67 | const newConfig = React.useMemo( 68 | () => ({ 69 | ...(configContextValue || defaultConfig), 70 | ...config, 71 | }), 72 | [config, configContextValue] 73 | ) 74 | 75 | if (!configContextValue) { 76 | defaultConfig = newConfig 77 | } 78 | 79 | return ( 80 | 81 | {children} 82 | 83 | ) 84 | } 85 | 86 | function useConfigContext() { 87 | return React.useContext(configContext) || defaultConfig 88 | } 89 | 90 | function makeQuery(options) { 91 | let initialData = options.config.paginated ? [] : null 92 | 93 | if (typeof options.config.initialData !== 'undefined') { 94 | initialData = options.config.initialData 95 | } 96 | 97 | let query = { 98 | ...options, 99 | pageVariables: [], 100 | instances: [], 101 | state: { 102 | error: null, 103 | isFetching: false, 104 | isFetchingMore: false, 105 | canFetchMore: false, 106 | failureCount: 0, 107 | isCached: false, 108 | isStale: true, 109 | data: initialData, 110 | }, 111 | // promise: null, 112 | // staleTimeout: null, 113 | // cacheTimeout: null, 114 | // cancelled: null, 115 | } 116 | 117 | query.setState = updater => { 118 | query.state = functionalUpdate(updater, query.state) 119 | query.instances.forEach(instance => { 120 | instance.onStateUpdate(query.state) 121 | }) 122 | globalStateListeners.forEach(d => d()) 123 | } 124 | 125 | query.subscribe = instance => { 126 | let found = query.instances.find(d => d.id === instance.id) 127 | 128 | if (found) { 129 | Object.assign(found, instance) 130 | } else { 131 | found = instance 132 | query.instances.push(instance) 133 | } 134 | 135 | // Mark as active 136 | query.setState(old => { 137 | return { 138 | ...old, 139 | isInactive: false, 140 | } 141 | }) 142 | 143 | // Cancel garbage collection 144 | clearTimeout(query.cacheTimeout) 145 | 146 | // Mark the query as not cancelled 147 | query.cancelled = null 148 | 149 | // Return the unsubscribe function 150 | return () => { 151 | query.instances = query.instances.filter(d => d.id !== instance.id) 152 | 153 | if (!query.instances.length) { 154 | // Cancel any side-effects 155 | query.cancelled = cancelledError 156 | 157 | if (query.cancelQueries) { 158 | query.cancelQueries() 159 | } 160 | 161 | // Mark as inactive 162 | query.setState(old => { 163 | return { 164 | ...old, 165 | isInactive: true, 166 | } 167 | }) 168 | 169 | // Schedule garbage collection 170 | query.cacheTimeout = setTimeout( 171 | () => { 172 | queries.splice(queries.findIndex(d => d === query), 1) 173 | globalStateListeners.forEach(d => d()) 174 | }, 175 | query.state.isCached ? query.config.cacheTime : 0 176 | ) 177 | } 178 | } 179 | } 180 | 181 | // Set up the fetch function 182 | const tryFetchQueryPages = async pageVariables => { 183 | try { 184 | // Perform the query 185 | const promises = pageVariables.map(variables => query.queryFn(variables)) 186 | 187 | query.cancelQueries = () => 188 | promises.map(({ cancel }) => cancel && cancel()) 189 | 190 | const data = await Promise.all(promises) 191 | 192 | if (query.cancelled) throw query.cancelled 193 | 194 | return data 195 | } catch (error) { 196 | if (query.cancelled) throw query.cancelled 197 | 198 | // If we fail, increase the failureCount 199 | query.setState(old => { 200 | return { 201 | ...old, 202 | failureCount: old.failureCount + 1, 203 | } 204 | }) 205 | 206 | // Do we need to retry the request? 207 | if ( 208 | // Only retry if the document is visible 209 | query.config.retry === true || 210 | query.state.failureCount < query.config.retry 211 | ) { 212 | if (!isDocumentVisible()) { 213 | return new Promise(r => {}) 214 | } 215 | 216 | // Determine the retryDelay 217 | const delay = functionalUpdate( 218 | query.config.retryDelay, 219 | query.state.failureCount 220 | ) 221 | 222 | // Return a new promise with the retry 223 | return new Promise((resolve, reject) => { 224 | // Keep track of the retry timeout 225 | setTimeout(async () => { 226 | if (query.cancelled) return reject(query.cancelled) 227 | 228 | try { 229 | const data = await tryFetchQueryPages(pageVariables) 230 | if (query.cancelled) return reject(query.cancelled) 231 | resolve(data) 232 | } catch (error) { 233 | if (query.cancelled) return reject(query.cancelled) 234 | reject(error) 235 | } 236 | }, delay) 237 | }) 238 | } 239 | 240 | throw error 241 | } 242 | } 243 | 244 | query.fetch = async ({ 245 | variables = query.config.paginated && query.state.isCached 246 | ? query.pageVariables 247 | : query.variables, 248 | force, 249 | isFetchMore, 250 | } = {}) => { 251 | // Don't refetch fresh queries without force 252 | if (!query.queryHash || (!query.state.isStale && !force)) { 253 | return 254 | } 255 | 256 | // Create a new promise for the query cache if necessary 257 | if (!query.promise) { 258 | query.promise = (async () => { 259 | // If there are any retries pending for this query, kill them 260 | query.cancelled = null 261 | 262 | const cleanup = () => { 263 | delete query.promise 264 | 265 | // Schedule a fresh invalidation, always! 266 | clearTimeout(query.staleTimeout) 267 | 268 | query.staleTimeout = setTimeout(() => { 269 | if (query) { 270 | query.setState(old => { 271 | return { 272 | ...old, 273 | isStale: true, 274 | } 275 | }) 276 | } 277 | }, query.config.staleTime) 278 | 279 | query.setState(old => { 280 | return { 281 | ...old, 282 | isFetching: false, 283 | isFetchingMore: false, 284 | } 285 | }) 286 | } 287 | 288 | try { 289 | // Set up the query refreshing state 290 | query.setState(old => { 291 | return { 292 | ...old, 293 | isFetching: true, 294 | isFetchingMore: isFetchMore, 295 | failureCount: 0, 296 | } 297 | }) 298 | 299 | variables = 300 | query.config.paginated && query.state.isCached && !isFetchMore 301 | ? variables 302 | : [variables] 303 | 304 | // Try to fetch 305 | let data = await tryFetchQueryPages(variables) 306 | 307 | // If we are paginating, and this is the first query or a fetch more 308 | // query, then store the variables in the pageVariables 309 | if ( 310 | query.config.paginated && 311 | (isFetchMore || !query.state.isCached) 312 | ) { 313 | query.pageVariables.push(variables[0]) 314 | } 315 | 316 | // Set data and mark it as cached 317 | query.setState(old => { 318 | data = query.config.paginated 319 | ? isFetchMore 320 | ? [...old.data, data[0]] 321 | : data 322 | : data[0] 323 | 324 | return { 325 | ...old, 326 | error: null, 327 | data, 328 | isCached: true, 329 | isStale: false, 330 | ...(query.config.paginated && { 331 | canFetchMore: query.config.getCanFetchMore( 332 | data[data.length - 1], 333 | data 334 | ), 335 | }), 336 | } 337 | }) 338 | 339 | query.instances.forEach( 340 | instance => 341 | instance.onSuccess && instance.onSuccess(query.state.data) 342 | ) 343 | 344 | cleanup() 345 | 346 | return data 347 | } catch (error) { 348 | // As long as it's not a cancelled retry 349 | cleanup() 350 | 351 | if (error !== query.cancelled) { 352 | // Store the error 353 | query.setState(old => { 354 | return { 355 | ...old, 356 | error, 357 | isCached: false, 358 | isStale: true, 359 | } 360 | }) 361 | 362 | query.instances.forEach( 363 | instance => instance.onError && instance.onError(error) 364 | ) 365 | 366 | throw error 367 | } 368 | } 369 | })() 370 | } 371 | 372 | return query.promise 373 | } 374 | 375 | query.setData = updater => 376 | query.setState(old => ({ 377 | ...old, 378 | data: functionalUpdate(updater, old.data), 379 | })) 380 | 381 | return query 382 | } 383 | 384 | export function useQuery(queryKey, queryFn, config = {}) { 385 | const isMountedRef = React.useRef(false) 386 | const wasSuspendedRef = React.useRef(false) 387 | const instanceIdRef = React.useRef(uid++) 388 | const instanceId = instanceIdRef.current 389 | 390 | config = { 391 | ...useConfigContext(), 392 | ...config, 393 | } 394 | 395 | const { manual } = config 396 | 397 | const [ 398 | queryHash, 399 | queryGroup, 400 | variablesHash, 401 | variables, 402 | ] = config.queryKeySerializerFn(queryKey) 403 | 404 | let query = queries.find(query => query.queryHash === queryHash) 405 | 406 | let wasPrefetched 407 | 408 | if (query) { 409 | wasPrefetched = query.config.prefetch 410 | query.config = config 411 | if (!isMountedRef.current) { 412 | query.config.prefetch = wasPrefetched 413 | } 414 | query.queryFn = queryFn 415 | } else { 416 | query = makeQuery({ 417 | queryHash, 418 | queryGroup, 419 | variablesHash, 420 | variables, 421 | config, 422 | queryFn, 423 | }) 424 | if (!isServer) { 425 | queries.push(query) 426 | } 427 | } 428 | 429 | React.useEffect(() => { 430 | if (config.refetchInterval && !query.refetchInterval) { 431 | query.refetchInterval = setInterval(() => { 432 | if (isDocumentVisible() || config.refetchIntervalInBackground) { 433 | query.fetch() 434 | } 435 | }, config.refetchInterval) 436 | 437 | return () => { 438 | clearInterval(query.refetchInterval) 439 | query.refetchInterval = null 440 | } 441 | } 442 | }, [config.refetchInterval, config.refetchIntervalInBackground, query]) 443 | 444 | const [state, setState] = React.useState(query.state) 445 | 446 | const onStateUpdate = React.useCallback(newState => setState(newState), []) 447 | const getLatestOnError = useGetLatest(config.onError) 448 | const getLatestOnSuccess = useGetLatest(config.onSuccess) 449 | 450 | React.useEffect(() => { 451 | const unsubscribeFromQuery = query.subscribe({ 452 | id: instanceId, 453 | onStateUpdate, 454 | onSuccess: data => getLatestOnSuccess() && getLatestOnSuccess()(data), 455 | onError: err => getLatestOnError() && getLatestOnError()(err), 456 | }) 457 | return unsubscribeFromQuery 458 | }, [getLatestOnError, getLatestOnSuccess, instanceId, onStateUpdate, query]) 459 | 460 | const isLoading = !state.isCached && query.state.isFetching 461 | const refetch = query.fetch 462 | const setData = query.setData 463 | 464 | const fetchMore = React.useCallback( 465 | config.paginated 466 | ? paginationVariables => 467 | query.fetch({ 468 | variables: paginationVariables, 469 | force: true, 470 | isFetchMore: true, 471 | }) 472 | : undefined, 473 | [query] 474 | ) 475 | 476 | const getLatestManual = useGetLatest(manual) 477 | 478 | React.useEffect(() => { 479 | if (getLatestManual()) { 480 | return 481 | } 482 | 483 | if (config.suspense) { 484 | if (wasSuspendedRef.current || wasPrefetched) { 485 | return 486 | } 487 | } 488 | 489 | const runRefetch = async () => { 490 | try { 491 | await query.fetch() 492 | } catch (err) { 493 | console.error(err) 494 | // Swallow this error. Don't rethrow it into a render function 495 | } 496 | } 497 | 498 | runRefetch() 499 | }, [config.suspense, getLatestManual, query, wasPrefetched]) 500 | 501 | React.useEffect(() => { 502 | isMountedRef.current = true 503 | }, []) 504 | 505 | if (config.suspense) { 506 | if (state.error) { 507 | throw state.error 508 | } 509 | if (!state.isCached) { 510 | wasSuspendedRef.current = true 511 | throw query.fetch() 512 | } 513 | } 514 | 515 | wasSuspendedRef.current = false 516 | 517 | return { 518 | ...state, 519 | isLoading, 520 | refetch, 521 | fetchMore, 522 | setData, 523 | } 524 | } 525 | 526 | export async function prefetchQuery(queryKey, queryFn, config = {}) { 527 | config = { 528 | ...defaultConfig, 529 | ...config, 530 | prefetch: true, 531 | } 532 | 533 | const [ 534 | queryHash, 535 | queryGroup, 536 | variablesHash, 537 | variables, 538 | ] = defaultConfig.queryKeySerializerFn(queryKey) 539 | 540 | // If we're prefetching, use the queryFn to make the fetch call 541 | 542 | let query = queries.find(query => query.queryHash === queryHash) 543 | 544 | if (query) { 545 | if (!config.force) { 546 | return 547 | } 548 | query.config = config 549 | query.queryFn = queryFn 550 | } else { 551 | query = makeQuery({ 552 | queryHash, 553 | queryGroup, 554 | variablesHash, 555 | variables, 556 | config, 557 | queryFn, 558 | }) 559 | if (!isServer) { 560 | queries.push(query) 561 | } 562 | } 563 | 564 | // Trigger a query subscription with one-time unique id 565 | const unsubscribeFromQuery = query.subscribe({ 566 | id: uid++, 567 | onStateUpdate: () => {}, 568 | }) 569 | 570 | // Trigger a fetch and return the promise 571 | try { 572 | return await query.fetch({ force: config.force }) 573 | } finally { 574 | // Since this is not a hook, upsubscribe after we're done 575 | unsubscribeFromQuery() 576 | } 577 | } 578 | 579 | export async function refetchQuery(queryKey, config = {}) { 580 | const [ 581 | , 582 | queryGroup, 583 | variablesHash, 584 | variables, 585 | ] = defaultConfig.queryKeySerializerFn(queryKey) 586 | 587 | // If we're simply refetching an existing query, then go find them 588 | // and call their fetch functions 589 | 590 | if (!queryGroup) { 591 | return 592 | } 593 | 594 | return Promise.all( 595 | queries.map(async query => { 596 | if (query.queryGroup !== queryGroup) { 597 | return 598 | } 599 | 600 | if (variables === false && query.variablesHash) { 601 | return 602 | } 603 | 604 | if (variablesHash && query.variablesHash !== variablesHash) { 605 | return 606 | } 607 | 608 | await query.fetch({ force: config.force }) 609 | }) 610 | ) 611 | } 612 | 613 | export function useMutation( 614 | mutationFn, 615 | { refetchQueries, refetchQueriesOnFailure } = {} 616 | ) { 617 | const [data, setData] = React.useState(null) 618 | const [error, setError] = React.useState(null) 619 | const [isLoading, setIsLoading] = React.useState(false) 620 | const mutationFnRef = React.useRef() 621 | mutationFnRef.current = mutationFn 622 | 623 | const mutate = React.useCallback( 624 | async (variables, { updateQuery, waitForRefetchQueries = false } = {}) => { 625 | setIsLoading(true) 626 | setError(null) 627 | 628 | const doRefetchQueries = async () => { 629 | const refetchPromises = refetchQueries.map(queryKey => 630 | refetchQuery(queryKey, { force: true }) 631 | ) 632 | if (waitForRefetchQueries) { 633 | await Promise.all(refetchPromises) 634 | } 635 | } 636 | 637 | try { 638 | const res = await mutationFnRef.current(variables) 639 | setData(res) 640 | 641 | if (updateQuery) { 642 | setQueryData(updateQuery, res, { shouldRefetch: false }) 643 | } 644 | 645 | if (refetchQueries) { 646 | try { 647 | await doRefetchQueries() 648 | } catch (err) { 649 | console.error(err) 650 | // Swallow this error since it is a side-effect 651 | } 652 | } 653 | 654 | setIsLoading(false) 655 | 656 | return res 657 | } catch (error) { 658 | setError(error) 659 | 660 | if (refetchQueriesOnFailure) { 661 | await doRefetchQueries() 662 | } 663 | 664 | setIsLoading(false) 665 | throw error 666 | } 667 | }, 668 | [refetchQueriesOnFailure, refetchQueries] 669 | ) 670 | 671 | return [mutate, { data, isLoading, error }] 672 | } 673 | 674 | export function useIsFetching() { 675 | const [state, setState] = React.useState({}) 676 | const ref = React.useRef() 677 | 678 | if (!ref.current) { 679 | ref.current = () => { 680 | setState({}) 681 | } 682 | globalStateListeners.push(ref.current) 683 | } 684 | 685 | React.useEffect(() => { 686 | return () => { 687 | globalStateListeners = globalStateListeners.filter(d => d !== ref.current) 688 | } 689 | }, []) 690 | 691 | return React.useMemo( 692 | () => state && queries.some(query => query.state.isFetching), 693 | [state] 694 | ) 695 | } 696 | 697 | export function setQueryData( 698 | userQueryKey, 699 | updater, 700 | { shouldRefetch = true } = {} 701 | ) { 702 | const [queryHash] = defaultConfig.queryKeySerializerFn(userQueryKey) 703 | 704 | if (!queryHash) { 705 | return 706 | } 707 | 708 | const query = queries.find(d => d.queryHash === queryHash) 709 | 710 | if (!query) { 711 | return 712 | } 713 | 714 | query.setData(updater) 715 | 716 | if (shouldRefetch) { 717 | return refetchQuery(userQueryKey) 718 | } 719 | } 720 | 721 | export async function refetchAllQueries({ 722 | includeInactive, 723 | force = includeInactive, 724 | shouldRefetchQuery, 725 | } = {}) { 726 | return Promise.all( 727 | queries.map(async query => { 728 | if ( 729 | typeof shouldRefetchQuery !== 'undefined' && 730 | !shouldRefetchQuery(query) 731 | ) { 732 | return 733 | } 734 | if (query.instances.length || includeInactive) { 735 | return query.fetch({ force }) 736 | } 737 | }) 738 | ) 739 | } 740 | 741 | export function clearQueryCache() { 742 | queries.length = 0 743 | } 744 | 745 | function defaultQueryKeySerializerFn(queryKey) { 746 | if (!queryKey) { 747 | return [] 748 | } 749 | 750 | if (typeof queryKey === 'function') { 751 | try { 752 | return defaultQueryKeySerializerFn(queryKey()) 753 | } catch { 754 | return [] 755 | } 756 | } 757 | 758 | if (Array.isArray(queryKey)) { 759 | let [id, variables] = queryKey 760 | const variablesIsObject = isObject(variables) 761 | 762 | if (typeof id !== 'string' || (variables && !variablesIsObject)) { 763 | console.warn('Tuple queryKey:', queryKey) 764 | throw new Error( 765 | `Invalid query key tuple type: [${typeof id}, and ${typeof variables}]` 766 | ) 767 | } 768 | 769 | const variablesHash = variablesIsObject ? stableStringify(variables) : '' 770 | 771 | return [ 772 | `${id}${variablesHash ? `_${variablesHash}` : ''}`, 773 | id, 774 | variablesHash, 775 | variables, 776 | ] 777 | } 778 | 779 | return [queryKey, queryKey] 780 | } 781 | 782 | function stableStringifyReplacer(_, value) { 783 | return isObject(value) 784 | ? Object.assign( 785 | {}, 786 | ...Object.keys(value) 787 | .sort() 788 | .map(key => ({ 789 | [key]: value[key], 790 | })) 791 | ) 792 | : Array.isArray(value) 793 | ? value 794 | : String(value) 795 | } 796 | 797 | export function stableStringify(obj) { 798 | return JSON.stringify(obj, stableStringifyReplacer) 799 | } 800 | 801 | function isObject(a) { 802 | return a && typeof a === 'object' && !Array.isArray(a) 803 | } 804 | 805 | function isDocumentVisible() { 806 | return ( 807 | typeof document === 'undefined' || 808 | document.visibilityState === undefined || 809 | document.visibilityState === 'visible' || 810 | document.visibilityState === 'prerender' 811 | ) 812 | } 813 | 814 | function isOnline() { 815 | return navigator.onLine === undefined || navigator.onLine 816 | } 817 | 818 | function useGetLatest(obj) { 819 | const ref = React.useRef() 820 | ref.current = obj 821 | 822 | return React.useCallback(() => ref.current, []) 823 | } 824 | 825 | function functionalUpdate(updater, old) { 826 | return typeof updater === 'function' ? updater(old) : updater 827 | } 828 | --------------------------------------------------------------------------------