Skip to content

Commit 963af60

Browse files
msutkowskiphryneas
andauthored
Implement refetchOnFocus, refetchOnReconnect (#90)
Co-authored-by: Lenz Weber <[email protected]>
1 parent 151c777 commit 963af60

27 files changed

+780
-100
lines changed

docs/api/createApi.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ The main point where you will define a service to use in your application.
2121
serializeQueryArgs?: SerializeQueryArgs<InternalQueryArgs>;
2222
keepUnusedDataFor?: number; // value is in seconds
2323
refetchOnMountOrArgChange?: boolean | number; // value is in seconds
24+
refetchOnFocus?: boolean;
25+
refetchOnReconnect?: boolean;
2426
```
2527

2628
### `baseQuery`
@@ -216,12 +218,36 @@ Defaults to `false`. This setting allows you to control whether RTK Query will o
216218
- `true` - Will always refetch when a new subscriber to a query is added. Behaves the same as calling the `refetch` callback or passing `forceRefetch: true` in the action creator.
217219
- `number` - **Value is in seconds**. If a number is provided and there is an existing query in the cache, it will compare the current time vs the last fulfilled timestamp, and only refetch if enough time has elapsed.
218220
219-
If you specify this option as well as with `skip: true`, this **will not be evaluated** until `skip` is false.
221+
If you specify this option alongside `skip: true`, this **will not be evaluated** until `skip` is false.
220222
221223
:::note
222224
You can set this globally in `createApi`, but you can also override the default value and have more granular control by passing `refetchOnMountOrArgChange` to each individual hook call or when dispatching the [`initiate`](#initiate) action.
223225
:::
224226
227+
### `refetchOnFocus`
228+
229+
Defaults to `false`. This setting allows you to control whether RTK Query will try to refetch all subscribed queries after the application window regains focus.
230+
231+
If you specify this option alongside `skip: true`, this **will not be evaluated** until `skip` is false.
232+
233+
:::note
234+
You can set this globally in `createApi`, but you can also override the default value and have more granular control by passing `refetchOnFocus` to each individual hook call or when dispatching the [`initiate`](#initiate) action.
235+
236+
If you specify `track: false` when manually dispatching queries, RTK Query will not be able to automatically refetch for you.
237+
:::
238+
239+
### `refetchOnReconnect`
240+
241+
Defaults to `false`. This setting allows you to control whether RTK Query will try to refetch all subscribed queries after regaining a network connection.
242+
243+
If you specify this option alongside `skip: true`, this **will not be evaluated** until `skip` is false.
244+
245+
:::note
246+
You can set this globally in `createApi`, but you can also override the default value and have more granular control by passing `refetchOnReconnect` to each individual hook call or when dispatching the [`initiate`](#initiate) action.
247+
248+
If you specify `track: false` when manually dispatching queries, RTK Query will not be able to automatically refetch for you.
249+
:::
250+
225251
## Return value
226252
227253
- [`reducerPath`](#reducerpath)

docs/api/setupListeners.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
id: setupListeners
3+
title: setupListeners
4+
sidebar_label: setupListeners
5+
hide_title: true
6+
hide_table_of_contents: false
7+
---
8+
9+
# `setupListeners`
10+
11+
A utility used to enable `refetchOnMount` and `refetchOnReconnect` behaviors. It requires the `dispatch` method from your store. Calling `setupListeners(store.dispatch)` will configure listeners with the recommended defaults, but you have the optional of providing a callback for more granular control.
12+
13+
```ts title="setupListeners default configuration"
14+
let initialized = false;
15+
export function setupListeners(
16+
dispatch: ThunkDispatch<any, any, any>,
17+
customHandler?: (
18+
dispatch: ThunkDispatch<any, any, any>,
19+
actions: {
20+
onFocus: typeof onFocus;
21+
onFocusLost: typeof onFocusLost;
22+
onOnline: typeof onOnline;
23+
onOffline: typeof onOffline;
24+
}
25+
) => () => void
26+
) {
27+
function defaultHandler() {
28+
const handleFocus = () => dispatch(onFocus());
29+
const handleFocusLost = () => dispatch(onFocusLost());
30+
const handleOnline = () => dispatch(onOnline());
31+
const handleOffline = () => dispatch(onOffline());
32+
const handleVisibilityChange = () => {
33+
if (window.document.visibilityState === 'visible') {
34+
handleFocus();
35+
} else {
36+
handleFocusLost();
37+
}
38+
};
39+
40+
if (!initialized) {
41+
if (typeof window !== 'undefined' && window.addEventListener) {
42+
// Handle focus events
43+
window.addEventListener('visibilitychange', handleVisibilityChange, false);
44+
window.addEventListener('focus', handleFocus, false);
45+
46+
// Handle connection events
47+
window.addEventListener('online', handleOnline, false);
48+
window.addEventListener('offline', handleOffline, false);
49+
initialized = true;
50+
}
51+
}
52+
const unsubscribe = () => {
53+
window.removeEventListener('focus', handleFocus);
54+
window.removeEventListener('visibilitychange', handleVisibilityChange);
55+
window.removeEventListener('online', handleOnline);
56+
window.removeEventListener('offline', handleOffline);
57+
initialized = false;
58+
};
59+
return unsubscribe;
60+
}
61+
62+
return customHandler ? customHandler(dispatch, { onFocus, onFocusLost, onOffline, onOnline }) : defaultHandler();
63+
}
64+
```
65+
66+
If you notice, `onFocus`, `onFocusLost`, `onOffline`, `onOnline` are all actions that are provided to the callback. Additionally, these action are made available to `api.internalActions` and are able to be used by dispatching them like this:
67+
68+
```ts title="Manual onFocus event"
69+
dispatch(api.internalActions.onFocus())`
70+
```

docs/concepts/queries.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@ Hooks are automatically generated based on the name of the `endpoint` in the ser
2121

2222
#### Query Hook Options
2323

24-
- [pollingInterval?](./polling) - Defaults to `0`
25-
- [refetchOnMountOrArgChange?](../api/createApi#refetchOnMountOrArgChange) - Defaults to `false`. Will override any value that was specified in `createApi`.
26-
- [skip?](./conditional-fetching) - Defaults to `false`
24+
- [skip](./conditional-fetching) - Defaults to `false`
25+
- [pollingInterval](./polling) - Defaults to `0` _(off)_
26+
- [refetchOnMountOrArgChange](../api/createApi#refetchonmountorargchange) - Defaults to `false`
27+
- [refetchOnFocus](../api/createApi#refetchonfocus) - Defaults to `false`
28+
- [refetchOnReconnect](../api/createApi#refetchonreconnect) - Defaults to `false`
29+
30+
> All `refetch`-related options will override the defaults you may have set in [createApi](../api/createApi)
2731
2832
Here is an example of a `PostDetail` component:
2933

docs/introduction/getting-started.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ With RTK Query, you usually define your entire API definition in one place. This
6969
An RTK service generates a "slice reducer" that should be included in the Redux root reducer, and a custom middleware that handles the data fetching. Both need to be added to the Redux store.
7070

7171
```ts title="src/store.ts"
72-
import { configureStore } from '@reduxjs/toolkit';
72+
import { configureStore, setupListeners } from '@reduxjs/toolkit';
7373
import { pokemonApi } from './services/pokemon';
7474

7575
export const store = configureStore({
@@ -85,6 +85,10 @@ export const store = configureStore({
8585
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(pokemonApi.middleware),
8686
// highlight-end
8787
});
88+
89+
// optional, but required for refetchOnFocus/refetchOnReconnect behaviors
90+
// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
91+
setupListeners(store.dispatch);
8892
```
8993

9094
### Wrap your application with the `Provider`

examples/react/src/app/services/times.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export const timeApi = createApi({
1414
provides: (_, id) => [{ type: 'Time', id }],
1515
}),
1616
}),
17+
refetchOnReconnect: true,
18+
refetchOnMountOrArgChange: 10,
1719
});
1820

1921
export const { usePrefetch: usePrefetchTime, useGetTimeQuery } = timeApi;

examples/react/src/app/store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { timeApi } from './services/times';
66
import polling from '../features/polling/pollingSlice';
77
import { splitApi } from './services/split';
88
import auth from '../features/auth/authSlice';
9+
import { setupListeners } from '@rtk-incubator/rtk-query';
910

1011
export const createStore = (options?: ConfigureStoreOptions['preloadedState'] | undefined) =>
1112
configureStore({
@@ -23,6 +24,7 @@ export const createStore = (options?: ConfigureStoreOptions['preloadedState'] |
2324
});
2425

2526
export const store = createStore();
27+
setupListeners(store.dispatch);
2628

2729
export type AppDispatch = typeof store.dispatch;
2830
export const useAppDispatch = () => useDispatch<AppDispatch>();

examples/react/src/features/time/TimeList.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ const TimeDisplay = ({ offset, label }: { offset: string; label: string }) => {
8080
const [pollingInterval, setPollingInterval] = React.useState(0);
8181
const { data, refetch, isFetching } = useGetTimeQuery(offset, {
8282
pollingInterval: canPoll ? pollingInterval : 0,
83-
refetchOnMountOrArgChange: 10,
83+
refetchOnReconnect: false,
84+
refetchOnFocus: true,
8485
});
8586

8687
return (

src/ApiProvider.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { configureStore } from '@reduxjs/toolkit';
22
import React from 'react';
33
import { Provider } from 'react-redux';
4+
import { setupListeners } from './setupListeners';
45
import { Api } from './apiTypes';
56

67
/**
@@ -9,7 +10,11 @@ import { Api } from './apiTypes';
910
* conflict with each other - please use the traditional redux setup
1011
* in that case.
1112
*/
12-
export function ApiProvider<A extends Api<any, {}, any, string>>(props: { children: any; api: A }) {
13+
export function ApiProvider<A extends Api<any, {}, any, string>>(props: {
14+
children: any;
15+
api: A;
16+
setupListeners?: Parameters<typeof setupListeners>[1];
17+
}) {
1318
const [store] = React.useState(() =>
1419
configureStore({
1520
reducer: {
@@ -18,6 +23,8 @@ export function ApiProvider<A extends Api<any, {}, any, string>>(props: { childr
1823
middleware: (gDM) => gDM().concat(props.api.middleware),
1924
})
2025
);
26+
// Adds the event listeners for online/offline/focus/etc
27+
setupListeners(store.dispatch, props.setupListeners);
2128

2229
return <Provider store={store}>{props.children}</Provider>;
2330
}

src/apiState.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ export type QueryCacheKey = string & { _type: 'queryCacheKey' };
1414
export type QuerySubstateIdentifier = { queryCacheKey: QueryCacheKey };
1515
export type MutationSubstateIdentifier = { requestId: string };
1616

17+
export type RefetchConfigOptions = {
18+
refetchOnMountOrArgChange: boolean | number;
19+
refetchOnReconnect: boolean;
20+
refetchOnFocus: boolean;
21+
};
22+
1723
export enum QueryStatus {
1824
uninitialized = 'uninitialized',
1925
pending = 'pending',
@@ -61,7 +67,11 @@ export function getRequestStatusFlags(status: QueryStatus): RequestStatusFlags {
6167
} as any;
6268
}
6369

64-
export type SubscriptionOptions = { pollingInterval?: number };
70+
export type SubscriptionOptions = {
71+
pollingInterval?: number;
72+
refetchOnReconnect?: boolean;
73+
refetchOnFocus?: boolean;
74+
};
6575
export type Subscribers = { [requestId: string]: SubscriptionOptions };
6676
export type QueryKeys<Definitions extends EndpointDefinitions> = {
6777
[K in keyof Definitions]: Definitions[K] extends QueryDefinition<any, any, any, any> ? K : never;
@@ -144,6 +154,7 @@ export type CombinedState<D extends EndpointDefinitions, E extends string> = {
144154
mutations: MutationState<D>;
145155
provided: InvalidationState<E>;
146156
subscriptions: SubscriptionState;
157+
config: ConfigState;
147158
};
148159

149160
export type InvalidationState<EntityTypes extends string> = {
@@ -161,6 +172,15 @@ export type SubscriptionState = {
161172
[queryCacheKey: string]: Subscribers | undefined;
162173
};
163174

175+
export type ConfigState = {
176+
online: boolean;
177+
focused: boolean;
178+
} & ModifiableConfigState;
179+
180+
export type ModifiableConfigState = {
181+
keepUnusedDataFor: number;
182+
} & RefetchConfigOptions;
183+
164184
export type MutationState<D extends EndpointDefinitions> = {
165185
[requestId: string]: MutationSubState<D[string]> | undefined;
166186
};

src/apiTypes.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { UnionToIntersection } from './tsHelpers';
1616
import { TS41Hooks } from './ts41Types';
1717
import './buildSelectors';
1818
import { SliceActions } from './buildSlice';
19+
import { onFocus, onFocusLost, onOffline, onOnline } from './setupListeners';
1920

2021
type UnwrapPromise<T> = T extends PromiseLike<infer V> ? V : T;
2122
type MaybePromise<T> = T | PromiseLike<T>;
@@ -113,4 +114,17 @@ export type ApiWithInjectedEndpoints<
113114

114115
export type InternalActions = SliceActions & {
115116
prefetchThunk: (endpointName: any, arg: any, options: PrefetchOptions) => ThunkAction<void, any, any, AnyAction>;
117+
} & {
118+
/**
119+
* Will cause the RTK Query middleware to trigger any refetchOnReconnect-related behavior
120+
* @link https://rtk-query-docs.netlify.app/api/setupListeners
121+
*/
122+
onOnline: typeof onOnline;
123+
onOffline: typeof onOffline;
124+
/**
125+
* Will cause the RTK Query middleware to trigger any refetchOnFocus-related behavior
126+
* @link https://rtk-query-docs.netlify.app/api/setupListeners
127+
*/
128+
onFocus: typeof onFocus;
129+
onFocusLost: typeof onFocusLost;
116130
};

src/buildActionMaps.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ declare module './apiTypes' {
2323

2424
export interface StartQueryActionCreatorOptions {
2525
subscribe?: boolean;
26-
forceRefetch?: boolean;
26+
forceRefetch?: boolean | number;
2727
subscriptionOptions?: SubscriptionOptions;
28-
refetchOnMountOrArgChange?: boolean | number;
2928
}
3029

3130
type StartQueryActionCreator<D extends QueryDefinition<any, any, any, any, any>> = (
@@ -81,14 +80,13 @@ export function buildActionMaps<Definitions extends EndpointDefinitions, Interna
8180
function buildQueryAction(endpoint: string, definition: QueryDefinition<any, any, any, any>) {
8281
const queryAction: StartQueryActionCreator<any> = (
8382
arg,
84-
{ subscribe = true, forceRefetch = false, refetchOnMountOrArgChange = false, subscriptionOptions } = {}
83+
{ subscribe = true, forceRefetch, subscriptionOptions } = {}
8584
) => (dispatch, getState) => {
8685
const internalQueryArgs = definition.query(arg);
8786
const queryCacheKey = serializeQueryArgs({ queryArgs: arg, internalQueryArgs, endpoint });
8887
const thunk = queryThunk({
8988
subscribe,
9089
forceRefetch,
91-
refetchOnMountOrArgChange,
9290
subscriptionOptions,
9391
endpoint,
9492
originalArgs: arg,

src/buildHooks.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,15 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
118118
}
119119

120120
function buildQueryHook(name: string): QueryHook<any> {
121-
return (arg: any, { refetchOnMountOrArgChange = false, skip = false, pollingInterval = 0 } = {}) => {
121+
return (
122+
arg: any,
123+
{ refetchOnReconnect, refetchOnFocus, refetchOnMountOrArgChange, skip = false, pollingInterval = 0 } = {}
124+
) => {
122125
const { select, initiate } = api.endpoints[name] as ApiEndpointQuery<
123126
QueryDefinition<any, any, any, any, any>,
124127
Definitions
125128
>;
126129
const dispatch = useDispatch<ThunkDispatch<any, any, AnyAction>>();
127-
128130
const stableArg = useShallowStableValue(arg);
129131

130132
const lastData = useRef<ResultTypeFrom<Definitions[string]> | undefined>();
@@ -141,15 +143,27 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
141143
const lastPromise = promiseRef.current;
142144
if (lastPromise && lastPromise.arg === stableArg) {
143145
// arg did not change, but options did probably, update them
144-
lastPromise.updateSubscriptionOptions({ pollingInterval });
146+
lastPromise.updateSubscriptionOptions({ pollingInterval, refetchOnReconnect, refetchOnFocus });
145147
} else {
146148
if (lastPromise) lastPromise.unsubscribe();
147149
const promise = dispatch(
148-
initiate(stableArg, { subscriptionOptions: { pollingInterval }, refetchOnMountOrArgChange })
150+
initiate(stableArg, {
151+
subscriptionOptions: { pollingInterval, refetchOnReconnect, refetchOnFocus },
152+
forceRefetch: refetchOnMountOrArgChange,
153+
})
149154
);
150155
promiseRef.current = promise;
151156
}
152-
}, [stableArg, dispatch, skip, pollingInterval, refetchOnMountOrArgChange, initiate]);
157+
}, [
158+
stableArg,
159+
dispatch,
160+
skip,
161+
pollingInterval,
162+
refetchOnMountOrArgChange,
163+
refetchOnFocus,
164+
refetchOnReconnect,
165+
initiate,
166+
]);
153167

154168
useEffect(() => {
155169
return () => void promiseRef.current?.unsubscribe();

0 commit comments

Comments
 (0)