Skip to content

Commit fe98d53

Browse files
authored
feat: remove idle state (#3302)
* feat: remove idle state in favor of status: loading & fetchStatus: idle * feat: remove idle state remove isIdle boolean * feat: remove idle state documentation around the removed idle state and the new fetchingStatus * feat: remove idle state add missing ' to docs
1 parent aa853d8 commit fe98d53

15 files changed

+180
-144
lines changed

docs/src/pages/guides/dependent-queries.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,33 @@ const { data: user } = useQuery(['user', email], getUserByEmail)
1212
const userId = user?.id
1313

1414
// Then get the user's projects
15-
const { isIdle, data: projects } = useQuery(
15+
const { status, fetchStatus, data: projects } = useQuery(
1616
['projects', userId],
1717
getProjectsByUser,
1818
{
1919
// The query will not execute until the userId exists
2020
enabled: !!userId,
2121
}
2222
)
23+
```
24+
25+
The `projects` query will start in:
26+
27+
```js
28+
status: 'loading'
29+
fetchStatus: 'idle'
30+
```
31+
32+
As soon as the `user` is available, the `projects` query will be `enabled` and will then transition to:
2333

24-
// isIdle will be `true` until `enabled` is true and the query begins to fetch.
25-
// It will then go to the `isLoading` stage and hopefully the `isSuccess` stage :)
34+
```js
35+
status: 'loading'
36+
fetchStatus: 'fetching'
37+
```
38+
39+
Once we have the projects, it will go to:
40+
41+
```js
42+
status: 'success'
43+
fetchStatus: 'idle'
2644
```

docs/src/pages/guides/disabling-queries.md

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,47 +10,80 @@ When `enabled` is `false`:
1010
- If the query has cached data
1111
- The query will be initialized in the `status === 'success'` or `isSuccess` state.
1212
- If the query does not have cached data
13-
- The query will start in the `status === 'idle'` or `isIdle` state.
13+
- The query will start in the `status === 'loading'` and `fetchStatus === 'idle'`
1414
- The query will not automatically fetch on mount.
15-
- The query will not automatically refetch in the background when new instances mount or new instances appearing
15+
- The query will not automatically refetch in the background
1616
- The query will ignore query client `invalidateQueries` and `refetchQueries` calls that would normally result in the query refetching.
17-
- `refetch` can be used to manually trigger the query to fetch.
17+
- `refetch` returned from `useQuery` can be used to manually trigger the query to fetch.
1818

19-
```js
19+
```jsx
2020
function Todos() {
2121
const {
22-
isIdle,
2322
isLoading,
2423
isError,
2524
data,
2625
error,
2726
refetch,
28-
isFetching,
27+
isFetching
2928
} = useQuery(['todos'], fetchTodoList, {
3029
enabled: false,
3130
})
3231

3332
return (
34-
<>
33+
<div>
3534
<button onClick={() => refetch()}>Fetch Todos</button>
3635

37-
{isIdle ? (
38-
'Not ready...'
39-
) : isLoading ? (
40-
<span>Loading...</span>
41-
) : isError ? (
42-
<span>Error: {error.message}</span>
43-
) : (
36+
{data ? (
4437
<>
4538
<ul>
4639
{data.map(todo => (
4740
<li key={todo.id}>{todo.title}</li>
4841
))}
4942
</ul>
50-
<div>{isFetching ? 'Fetching...' : null}</div>
5143
</>
44+
) : (
45+
isError ? (
46+
<span>Error: {error.message}</span>
47+
) : (
48+
(isLoading && !isFetching) ? (
49+
<span>Not ready ...</span>
50+
) : (
51+
<span>Loading...</span>
52+
)
53+
)
5254
)}
53-
</>
55+
56+
<div>{isFetching ? 'Fetching...' : null}</div>
57+
</div>
58+
)
59+
}
60+
```
61+
62+
Permanently disabling a query opts out of many great features that react-query has to offer (like background refetches), and it's also not the idiomatic way. It takes you from the declartive approach (defining dependencies when your query should run) into an imperative mode (fetch whenever I click here). It is also not possible to pass parameters to `refetch`. Oftentimes, all you want is a lazy query that defers the initial fetch:
63+
64+
## Lazy Queries
65+
66+
The enabled option can not only be used to permenantly disable a query, but also to enable / disable it at a later time. A good example would be a filter form where you only want to fire off the first request once the user has entered a filter value:
67+
68+
```jsx
69+
function Todos() {
70+
const [filter, setFilter] = React.useState('')
71+
72+
const { data } = useQuery(
73+
['todos', filter],
74+
() => fetchTodos(filter),
75+
{
76+
// ⬇️ disabled as long as the filter is empty
77+
enabled: !!filter
78+
}
79+
)
80+
81+
return (
82+
<div>
83+
// 🚀 applying the filter will enable and execute the query
84+
<FiltersForm onApply={setFilter} />
85+
{data && <TodosTable data={data}} />
86+
</div>
5487
)
5588
}
5689
```

docs/src/pages/guides/network-mode.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Since React Query is most often used for data fetching in combination with data
99

1010
## Network Mode: online
1111

12-
In this mode, Queries and Mutations will not fire unless you have network connection. This is the default mode. If a fetch is initiated for a query, it will always stay in the `state` (`loading`, `idle`, `error`, `success`) it is in if the fetch cannot be made because there is no network connection. However, a `fetchStatus` is exposed additionally. This can be either:
12+
In this mode, Queries and Mutations will not fire unless you have network connection. This is the default mode. If a fetch is initiated for a query, it will always stay in the `state` (`loading`, `error`, `success`) it is in if the fetch cannot be made because there is no network connection. However, a [fetchStatus](./queries#fetchstatus) is exposed additionally. This can be either:
1313

1414
- `fetching`: The `queryFn` is really executing - a request is in-flight.
1515
- `paused`: The query is not executing - it is `paused` until you have connection again
@@ -41,6 +41,6 @@ The [React Query Devtools](../devtools) will show Queries in a `paused` state if
4141

4242
## Signature
4343

44-
- `networkMode: 'online' | 'always' | 'offlineFirst`
44+
- `networkMode: 'online' | 'always' | 'offlineFirst'`
4545
- optional
4646
- defaults to `'online'`

docs/src/pages/guides/queries.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,14 @@ const result = useQuery(['todos'], fetchTodoList)
3232

3333
The `result` object contains a few very important states you'll need to be aware of to be productive. A query can only be in one of the following states at any given moment:
3434

35-
- `isLoading` or `status === 'loading'` - The query has no data and is currently fetching
35+
- `isLoading` or `status === 'loading'` - The query has no data yet
3636
- `isError` or `status === 'error'` - The query encountered an error
3737
- `isSuccess` or `status === 'success'` - The query was successful and data is available
38-
- `isIdle` or `status === 'idle'` - The query is currently disabled (you'll learn more about this in a bit)
3938

4039
Beyond those primary states, more information is available depending on the state of the query:
4140

4241
- `error` - If the query is in an `isError` state, the error is available via the `error` property.
4342
- `data` - If the query is in a `success` state, the data is available via the `data` property.
44-
- `isFetching` - In any state, if the query is fetching at any time (including background refetching) `isFetching` will be `true`.
4543

4644
For **most** queries, it's usually sufficient to check for the `isLoading` state, then the `isError` state, then finally, assume that the data is available and render the successful state:
4745

@@ -92,6 +90,28 @@ function Todos() {
9290
)
9391
}
9492
```
93+
94+
TypeScript will also narrow the type of `data` correctly if you've checked for `loading` and `error` before accessing it.
95+
96+
### FetchStatus
97+
98+
In addition to the `status` field, the `result` object, you will also get an additional `fetchStatus`property with the following options:
99+
100+
- `fetchStatus === 'fetching'` - The query is currently fetching.
101+
- `fetchStatus === 'paused'` - The query wanted to fetch, but it is paused. Read more about this in the [Network Mode](./network-mode) guide.
102+
- `fetchStatus === 'idle'` - The query is not doing anything at the moment.
103+
104+
### Why two different states?
105+
106+
Background refetches and stale-while-revalidate logic make all combinations for `status` and `fetchStatus` possible. For example:
107+
- a query in `success` status will usually be in `idle` fetchStatus, but it could also be in `fetching` if a background refetch is happening.
108+
- a query that mounts and has no data will usually be in `loading` status and `fetching` fetchStatus, but it could also be `paused` if there is no network connection.
109+
110+
So keep in mind that a query can be in `loading` state without actually fetching data. As a rule of thumb:
111+
112+
- The `status` gives information about the `data`: Do we have any or not?
113+
- The `fetchStatus` gives information about the `queryFn`: Is it running or not?
114+
95115
## Further Reading
96116

97117
For an alternative way of performing status checks, have a look at the [Community Resources](../community/tkdodos-blog#4-status-checks-in-react-query).

docs/src/pages/reference/useQuery.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,9 @@ const result = useQuery({
190190

191191
- `status: String`
192192
- Will be:
193-
- `idle` if the query is idle. This only happens if a query is initialized with `enabled: false` and no initial data is available.
194193
- `loading` if the query is in a "hard" loading state. This means there is no cached data and the query is currently fetching, eg `isFetching === true`
195194
- `error` if the query attempt resulted in an error. The corresponding `error` property has the error received from the attempted fetch
196195
- `success` if the query has received a response with no errors and is ready to display its data. The corresponding `data` property on the query is the data received from the successful fetch or if the query's `enabled` property is set to `false` and has not been fetched yet `data` is the first `initialData` supplied to the query on initialization.
197-
- `isIdle: boolean`
198-
- A derived boolean from the `status` variable above, provided for convenience.
199196
- `isLoading: boolean`
200197
- A derived boolean from the `status` variable above, provided for convenience.
201198
- `isSuccess: boolean`
@@ -229,8 +226,8 @@ const result = useQuery({
229226
- This property can be used to not show any previously cached data.
230227
- `fetchStatus: FetchStatus`
231228
- `fetching`: Is `true` whenever the queryFn is executing, which includes initial `loading` as well as background refetches.
232-
- `paused`: The query wanted to fetch, but has been `paused`
233-
- `idle`: The query is not fetching
229+
- `paused`: The query wanted to fetch, but has been `paused`.
230+
- `idle`: The query is not fetching.
234231
- see [Network Mode](../guides/network-mode) for more information.
235232
- `isFetching: boolean`
236233
- A derived boolean from the `fetchStatus` variable above, provided for convenience.

src/core/query.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ function getDefaultState<
618618
fetchFailureCount: 0,
619619
fetchMeta: null,
620620
isInvalidated: false,
621-
status: hasData ? 'success' : 'idle',
621+
status: hasData ? 'success' : 'loading',
622622
fetchStatus: 'idle',
623623
}
624624
}

src/core/queryObserver.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ export class QueryObserver<
489489
if (
490490
typeof options.placeholderData !== 'undefined' &&
491491
typeof data === 'undefined' &&
492-
(status === 'loading' || status === 'idle')
492+
status === 'loading'
493493
) {
494494
let placeholderData
495495

@@ -541,7 +541,6 @@ export class QueryObserver<
541541
isLoading: status === 'loading',
542542
isSuccess: status === 'success',
543543
isError: status === 'error',
544-
isIdle: status === 'idle',
545544
data,
546545
dataUpdatedAt,
547546
error,

src/core/tests/queriesObserver.test.tsx

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,20 @@ describe('queriesObserver', () => {
8888
unsubscribe()
8989
expect(results.length).toBe(6)
9090
expect(results[0]).toMatchObject([
91-
{ status: 'idle', data: undefined },
92-
{ status: 'idle', data: undefined },
91+
{ status: 'loading', fetchStatus: 'idle', data: undefined },
92+
{ status: 'loading', fetchStatus: 'idle', data: undefined },
9393
])
9494
expect(results[1]).toMatchObject([
95-
{ status: 'loading', data: undefined },
96-
{ status: 'idle', data: undefined },
95+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
96+
{ status: 'loading', fetchStatus: 'idle', data: undefined },
9797
])
9898
expect(results[2]).toMatchObject([
99-
{ status: 'loading', data: undefined },
100-
{ status: 'loading', data: undefined },
99+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
100+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
101101
])
102102
expect(results[3]).toMatchObject([
103103
{ status: 'success', data: 1 },
104-
{ status: 'loading', data: undefined },
104+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
105105
])
106106
expect(results[4]).toMatchObject([
107107
{ status: 'success', data: 1 },
@@ -138,20 +138,20 @@ describe('queriesObserver', () => {
138138
expect(queryCache.find(key2, { type: 'active' })).toBeUndefined()
139139
expect(results.length).toBe(6)
140140
expect(results[0]).toMatchObject([
141-
{ status: 'idle', data: undefined },
142-
{ status: 'idle', data: undefined },
141+
{ status: 'loading', fetchStatus: 'idle', data: undefined },
142+
{ status: 'loading', fetchStatus: 'idle', data: undefined },
143143
])
144144
expect(results[1]).toMatchObject([
145-
{ status: 'loading', data: undefined },
146-
{ status: 'idle', data: undefined },
145+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
146+
{ status: 'loading', fetchStatus: 'idle', data: undefined },
147147
])
148148
expect(results[2]).toMatchObject([
149-
{ status: 'loading', data: undefined },
150-
{ status: 'loading', data: undefined },
149+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
150+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
151151
])
152152
expect(results[3]).toMatchObject([
153153
{ status: 'success', data: 1 },
154-
{ status: 'loading', data: undefined },
154+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
155155
])
156156
expect(results[4]).toMatchObject([
157157
{ status: 'success', data: 1 },
@@ -183,20 +183,20 @@ describe('queriesObserver', () => {
183183
unsubscribe()
184184
expect(results.length).toBe(6)
185185
expect(results[0]).toMatchObject([
186-
{ status: 'idle', data: undefined },
187-
{ status: 'idle', data: undefined },
186+
{ status: 'loading', fetchStatus: 'idle', data: undefined },
187+
{ status: 'loading', fetchStatus: 'idle', data: undefined },
188188
])
189189
expect(results[1]).toMatchObject([
190-
{ status: 'loading', data: undefined },
191-
{ status: 'idle', data: undefined },
190+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
191+
{ status: 'loading', fetchStatus: 'idle', data: undefined },
192192
])
193193
expect(results[2]).toMatchObject([
194-
{ status: 'loading', data: undefined },
195-
{ status: 'loading', data: undefined },
194+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
195+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
196196
])
197197
expect(results[3]).toMatchObject([
198198
{ status: 'success', data: 1 },
199-
{ status: 'loading', data: undefined },
199+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
200200
])
201201
expect(results[4]).toMatchObject([
202202
{ status: 'success', data: 1 },
@@ -231,20 +231,20 @@ describe('queriesObserver', () => {
231231
unsubscribe()
232232
expect(results.length).toBe(5)
233233
expect(results[0]).toMatchObject([
234-
{ status: 'idle', data: undefined },
235-
{ status: 'idle', data: undefined },
234+
{ status: 'loading', fetchStatus: 'idle', data: undefined },
235+
{ status: 'loading', fetchStatus: 'idle', data: undefined },
236236
])
237237
expect(results[1]).toMatchObject([
238-
{ status: 'loading', data: undefined },
239-
{ status: 'idle', data: undefined },
238+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
239+
{ status: 'loading', fetchStatus: 'idle', data: undefined },
240240
])
241241
expect(results[2]).toMatchObject([
242-
{ status: 'loading', data: undefined },
243-
{ status: 'loading', data: undefined },
242+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
243+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
244244
])
245245
expect(results[3]).toMatchObject([
246246
{ status: 'success', data: 1 },
247-
{ status: 'loading', data: undefined },
247+
{ status: 'loading', fetchStatus: 'fetching', data: undefined },
248248
])
249249
expect(results[4]).toMatchObject([
250250
{ status: 'success', data: 1 },

src/core/tests/query.test.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,12 +264,14 @@ describe('query', () => {
264264
if (typeof AbortSignal === 'function') {
265265
expect(query.state).toMatchObject({
266266
data: undefined,
267-
status: 'idle',
267+
status: 'loading',
268+
fetchStatus: 'idle',
268269
})
269270
} else {
270271
expect(query.state).toMatchObject({
271272
data: 'data',
272273
status: 'success',
274+
fetchStatus: 'idle',
273275
dataUpdateCount: 1,
274276
})
275277
}
@@ -390,7 +392,7 @@ describe('query', () => {
390392
// The query should
391393
expect(queryFn).toHaveBeenCalledTimes(1) // have been called,
392394
expect(query.state.error).toBe(null) // not have an error, and
393-
expect(query.state.status).toBe('idle') // not be loading any longer
395+
expect(query.state.fetchStatus).toBe('idle') // not be loading any longer
394396
})
395397

396398
test('should be able to refetch a cancelled query', async () => {

0 commit comments

Comments
 (0)