Skip to content

Commit 90cc14b

Browse files
authored
Merge pull request #4176 from riqts/internal-documentation
2 parents c189792 + 45cc9ae commit 90cc14b

File tree

4 files changed

+393
-0
lines changed

4 files changed

+393
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# invalidationByTags
2+
3+
4+
## Overview
5+
`InvalidationByTagsHandler` is a handler instantiated during the (BuildMiddleware) step of the build. The handler acts as a (Middleware) and executes each step in response to matching of internal asyncThunk actions.
6+
7+
The matchers used for a "invalidation sequence" are these two cases:
8+
```ts no-transpile
9+
const isThunkActionWithTags = isAnyOf(
10+
isFulfilled(mutationThunk),
11+
isRejectedWithValue(mutationThunk),
12+
)
13+
14+
const isQueryEnd = isAnyOf(
15+
isFulfilled(mutationThunk, queryThunk),
16+
isRejected(mutationThunk, queryThunk),
17+
)
18+
```
19+
20+
## Triggers
21+
22+
The handler has 3 core conditionals that trigger a sequence:
23+
24+
*Conditional 1 AND 3 are identical in process except the tags are calculated from the payload rather than from the action and endpointDefinition*
25+
26+
1. Mutation trigger
27+
2. Query trigger
28+
3. Manual invalidation via `api.util.invalidateTags` trigger
29+
```ts no-transpile
30+
const handler: ApiMiddlewareInternalHandler = (action, mwApi) => {
31+
if (isThunkActionWithTags(action)) {
32+
invalidateTags(
33+
calculateProvidedByThunk(
34+
action,
35+
'invalidatesTags',
36+
endpointDefinitions,
37+
assertTagType,
38+
),
39+
mwApi,
40+
)
41+
} else if (isQueryEnd(action)) {
42+
invalidateTags([], mwApi)
43+
} else if (api.util.invalidateTags.match(action)) {
44+
invalidateTags(
45+
calculateProvidedBy(
46+
action.payload,
47+
undefined,
48+
undefined,
49+
undefined,
50+
undefined,
51+
assertTagType,
52+
),
53+
mwApi,
54+
)
55+
}
56+
}
57+
```
58+
59+
60+
## Core Sequence
61+
1. `invalidateTags()` initiates:
62+
1. invalidateTags function is called with a list of tags generated from the action metadata
63+
2. in the case of a [queryThunk] resolution an empty set of tags is always provided
64+
2. The tags calculated are added to the list of pending tags to invalidate (see [delayed](Delayed) )
65+
3. (optional: 'Delayed') the invalidateTags function is ended if the `apiSlice.invalidationBehaviour` is set to "delayed" and there are any pending thunks/queries running in that `apiSlice`
66+
4. Pending tags are reset to an empty list, if there are no tags the function ends here
67+
5. Selects all `{ endpointName, originalArgs, queryCacheKey }` combinations that would be invalidated by a specific set of tags.
68+
6. Iterates through queryCacheKeys selected and performs one of two actions if the query exists*
69+
1. removes cached query result - via the `removeQueryResult` action - if no subscription is active
70+
2. if the query is "uninitialized" it initiates a `refetchQuery` action
71+
```js no-transpile
72+
const toInvalidate = api.util.selectInvalidatedBy(rootState, tags);
73+
context.batch(() => {
74+
const valuesArray = Array.from(toInvalidate.values());
75+
for (const {
76+
queryCacheKey
77+
} of valuesArray) {
78+
const querySubState = state.queries[queryCacheKey];
79+
const subscriptionSubState = internalState.currentSubscriptions[queryCacheKey] ?? {};
80+
if (querySubState) {
81+
if (countObjectKeys(subscriptionSubState) === 0) {
82+
mwApi.dispatch(removeQueryResult({
83+
queryCacheKey
84+
}));
85+
} else if (querySubState.status !== "uninitialized" /* uninitialized */) {
86+
mwApi.dispatch(refetchQuery(querySubState, queryCacheKey));
87+
}
88+
}
89+
}
90+
});
91+
```
92+
93+
:::note
94+
Step 6 is performed within a `context.batch()` call.
95+
:::
96+
97+
### Delayed
98+
99+
RTKQ now has internal logic to delay tag invalidation briefly, to allow multiple invalidations to get handled together. This is controlled by a new `invalidationBehavior: 'immediate' | 'delayed'` flag on `createApi`. The new default behavior is `'delayed'`. Set it to `'immediate'` to revert to the behavior in RTK 1.9.
100+
101+
The `'delayed'` behaviour enables a check inside `invalidationByTags` that will cause any invalidation that is triggered while a query/mutation is still pending to batch the invalidation until no query/mutation is running.
102+
```ts no-transpile
103+
function invalidateTags(
104+
newTags: readonly FullTagDescription<string>[],
105+
mwApi: SubMiddlewareApi,
106+
) {
107+
const rootState = mwApi.getState()
108+
const state = rootState[reducerPath]
109+
110+
pendingTagInvalidations.push(...newTags)
111+
112+
if (
113+
state.config.invalidationBehavior === 'delayed' &&
114+
hasPendingRequests(state)
115+
) {
116+
return
117+
}
118+
```
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# BuildSlice
2+
3+
## Slices
4+
5+
### querySlice
6+
#### reducers
7+
- `removeQueryResult` - delete a specific cacheKey's stored result
8+
- `queryResultPatched` - patch a specific cacheKey's result
9+
#### extraReducers - matching queryThunk cases
10+
- `queryThunk.pending`
11+
- Initially sets QueryStatus to uninitialized
12+
- updates QueryStatus to pending
13+
- Generates requestId
14+
- stores originalArgs
15+
- stores startedTimeStamp
16+
- `queryThunk.fulfilled`
17+
- handles merge functionality first
18+
- otherwise updates the cache data, creates a fulfilledTimeStamp and deletes the substates error
19+
20+
```ts no-transpile
21+
if (merge) {
22+
if (substate.data !== undefined) {
23+
const { fulfilledTimeStamp, arg, baseQueryMeta, requestId } =
24+
meta
25+
// There's existing cache data. Let the user merge it in themselves.
26+
// We're already inside an Immer-powered reducer, and the user could just mutate `substate.data`
27+
// themselves inside of `merge()`. But, they might also want to return a new value.
28+
// Try to let Immer figure that part out, save the result, and assign it to `substate.data`.
29+
let newData = createNextState(
30+
substate.data,
31+
(draftSubstateData) => {
32+
// As usual with Immer, you can mutate _or_ return inside here, but not both
33+
return merge(draftSubstateData, payload, {
34+
arg: arg.originalArgs,
35+
baseQueryMeta,
36+
fulfilledTimeStamp,
37+
requestId,
38+
})
39+
},
40+
)
41+
substate.data = newData
42+
} else {
43+
// Presumably a fresh request. Just cache the response data.
44+
substate.data = payload
45+
}
46+
}
47+
```
48+
49+
- `queryThunk.rejected`
50+
- utilises `condition()` from `queryThunk` and does nothing if the rejection is a result of `condition()` (indicates a thunk is already running here)
51+
- else substate.error is set and the status is changed to rejected
52+
- `hasRehydrationInfo`
53+
- iterates through and resets entries for all fulfilled or rejected status
54+
55+
### mutationSlice
56+
#### reducers
57+
- `removeMutationResult`
58+
- calls `getMutationCacheKey` from payload
59+
- if cacheKey is in draft it deletes `draft[cacheKey`(?)
60+
#### extraReducers - matching mutationThunk cases
61+
- `mutationThunk.pending`
62+
- exits if track is set to false
63+
- otherwise updates appropriate cacheKey with requestId, pending status and startedTimeStamp
64+
- `mutationThunk.fulfilled`
65+
- exits if track is set to false
66+
- otherwise sets data off payload and fulfilledTimeStamp
67+
- `mutationThunk.rejected`
68+
- exits if track is set to false
69+
- otherwise sets error and status to rejected
70+
- `hasRehydrationInfo`
71+
- iterates through and resets entries for all fulfilled or rejected status
72+
73+
### invalidationSlice
74+
75+
#### reducers
76+
- updateProvidedBy
77+
- takes queryCacheKey and providedTags from payload
78+
- appends to a list of idSubscriptions the queryCacheKey that are currently subscribed to for each tag
79+
#### extraReducers
80+
- `querySlice.actions.removeQueryResult`,
81+
- deletes relevant queryCacheKey entry from list of subscription ids
82+
- `hasRehydrationInfo`
83+
- TODO
84+
- `queryThunk.fulfilled` or `queryThunk.rejected`
85+
- gets list of tags from action and endpoint definition
86+
- gets queryCacheKey
87+
- calls updateProvidedBy action
88+
89+
### subscriptionSlice / internalSubscriptionSlice
90+
#### reducers
91+
- updateSubscriptionOptions
92+
- unsubscribeQueryResult
93+
- internal_getRTKQSubscriptions
94+
- subscriptionsUpdated
95+
- applyPatches() to the state from the payload
96+
97+
### configSlice
98+
#### reducers
99+
- middlewareRegistered
100+
- toggles whether the middleware is registered or if there is a conflict
101+
#### extraReducers
102+
- `onOnline`
103+
- manages state.online in response to listenerMiddleware
104+
- `onOffline`
105+
- manages state.online in response to listenerMiddleware
106+
- `onFocus`
107+
- manages state.focused in response to listenerMiddleware
108+
- `onFocusLost`
109+
- manages state.focused in response to listenerMiddleware
110+
- `hasRehydrationInfo`
111+
- lists a comment that says: "update the state to be a new object to be picked up as a "state change" by redux-persist's `autoMergeLevel2`"
112+
113+
114+
## Functions
115+
### `updateQuerySubstateIfExists`
116+
Utility function that takes the api/endpoint state, queryCacheKey and Update function.
117+
The "SubState" is determined by accessing the `queryCacheKey` value inside the state. If the substate exists, the update function is executed on the substate.
118+
```js no-transpile
119+
function updateQuerySubstateIfExists(state, queryCacheKey, update) {
120+
const substate = state[queryCacheKey];
121+
if (substate) {
122+
update(substate);
123+
}
124+
}
125+
```
126+
127+
### `getMutationCacheKey`
128+
conditionally determines the cachekey to be used for the mutation, prioritising the argument provided, followed by the provided cacheKey, and the generated requestId otherwise
129+
```ts no-transpile
130+
export function getMutationCacheKey(
131+
id:
132+
| { fixedCacheKey?: string; requestId?: string }
133+
| MutationSubstateIdentifier
134+
| { requestId: string; arg: { fixedCacheKey?: string | undefined } },
135+
): string | undefined {
136+
return ('arg' in id ? id.arg.fixedCacheKey : id.fixedCacheKey) ?? id.requestId
137+
}
138+
```
139+
140+
### `getMutationSubstateIfExists`
141+
142+
same as query version except it uses the id instead of the queryCacheKey, and uses the `getMutationCacheKey` to determine the cachekey
143+
```js no-transpile
144+
function updateMutationSubstateIfExists(state, id, update) {
145+
const substate = state[getMutationCacheKey(id)];
146+
if (substate) {
147+
update(substate);
148+
}
149+
}
150+
```
151+

docs/rtk-query/internal/overview.mdx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# RTKQ internal
2+
3+
## Overview
4+
5+
> RTK Query is a powerful data fetching and caching tool built on top of Redux Toolkit. It is designed to simplify the process of fetching, caching, and updating server state in your application. It is built on top of Redux Toolkit and uses Redux internally.
6+
7+
This documentation is intended to provide a high-level overview of the internal architecture of RTK-Query. It is not intended to be a comprehensive guide to the library, but rather a guide to the internal architecture and how it works.
8+
9+
## createApi - The Entry Point
10+
When `createApi()` is called it takes the options provided and calls internally the `buildCreateApi()` function passing into it two modules:
11+
12+
*Modules are RTK-Query's method of customizing how the `createApi` method handles endpoints.*
13+
14+
- `coreModule()` - responsible for the majority of the internal handling using core redux logic i.e. slices, reducers, asyncThunks.
15+
- `reactHooksModule()` - a module that generates react hooks from endpoints using react-redux
16+
17+
## Core Module
18+
19+
The core module takes the `api` and the options passed to `createApi()`. In turn an internal set of "build" methods are called. Each of these build methods create a set of functions which are assigned to either `api.util` or `api.internalActions` and/or passed to a future "build" step.
20+
21+
### buildThunks
22+
RTK-Query's internal functionality operates using the same `asyncThunk` exposed from RTK. In the first "build" method, a number of thunks are generated for the core module to use:
23+
24+
- `queryThunk`
25+
- `mutationThunk`
26+
- `patchedQueryData`
27+
- `updateQueryData`
28+
- `upsertQueryData`
29+
- `prefetch`
30+
- `buildMatchThunkActions`
31+
32+
### buildSlice
33+
RTK-Query uses a very familiar redux-centric architecture. Where the `api` is a slice of your store, the `api` has its own slices created within it. These slices are where the majority of the RTKQ magic happens.
34+
35+
The slices built inside this "build" are:
36+
*Some of which have their own actions*
37+
- querySlice
38+
- mutationSlice
39+
- invalidationSlice
40+
- subscriptionSlice (used as a dummy slice to generate actions internally)
41+
- internalSubscriptionsSlice
42+
- configSlice (internal tracking of focus state, online state, hydration etc)
43+
44+
buildSlice also exposes the core action `resetApiState` which is subsequently added to the `api.util`
45+
46+
### buildMiddleware
47+
RTK-Query has a series of custom middlewares established within its store to handle additional responses in addition to the core logic established within the slices from buildSlice.
48+
49+
Each middleware built during this step is referred to internally as a "Handler" and are as follows:
50+
51+
- `buildDevCheckHandler
52+
- `buildCacheCollectionHandler
53+
- `buildInvalidationByTagsHandler
54+
- `buildPollingHandler
55+
- `buildCacheLifecycleHandler
56+
- `buildQueryLifecycleHandler
57+
58+
### buildSelectors
59+
build selectors is a crucial step that exposes to the `api` and utils:
60+
61+
- `buildQuerySelector
62+
- `buildMutationSelector
63+
- `selectInvalidatedBy
64+
- `selectCachedArgsForQuery
65+
66+
### return
67+
Finally each endpoint passed into the `createApi()` is iterated over and assigned either the query or the mutation selectors, initiators and match cases.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# queryThunk
2+
3+
## Overview
4+
A core action instantiated during (buildThunks). `queryThunk` leverages `createAsyncThunk` to initiate the query process and is used extensively throughout the codebase both as a match case and to initiate queries. The payload for the query is built using [executeEndpoint]
5+
6+
## Functions
7+
Before executing the payload creator function in [executeEndpoint], `queryThunk` executes two functions:
8+
9+
### `getPendingMeta()`
10+
1. `getPendingMeta()` - adds additional metadata to the action to be used in reducers or middleware.
11+
1. `startedTimeStamp`
12+
2. `SHOULD_AUTOBATCH`
13+
14+
### `condition()`
15+
16+
Performs conditional checks based on the provided args to decide whether the query should continue or not. (also attaches the field `dispatchConditionRejected: true` as confirmation that the condition was checked)
17+
```ts no-transpile
18+
if (isUpsertQuery(queryThunkArgs)) { return true }
19+
if (requestState?.status === "pending") { return false }
20+
if (isForcedQuery(queryThunkArgs, state)) { return true }
21+
if (isQueryDefinition(endpointDefinition) && endpointDefinition?.forceRefetch?.({ return true }
22+
if (fulfilledVal) { return false }
23+
else return true
24+
```
25+
26+
## Middleware Uses
27+
28+
### buildSlice
29+
The query endpoint is built almost entirely off of the `extraReducers` matching a `queryThunk` pending/fulfilled/rejected actions and updates the `querySubstate` plus meta data accordingly. The query slice utilises the condition and attached metadata created by a `queryThunk`.
30+
31+
`buildSlice` additionally matches resolved (rejected OR fulfilled) `queryThunks` to update providedTags.
32+
33+
### invalidationByTags
34+
matches against all rejected/fulfilled cases for `queryThunk`
35+
36+
### Polling
37+
matches against multiple queryThunk cases
38+
```js no-transpile
39+
if (queryThunk.pending.match(action) || queryThunk.rejected.match(action) && action.meta.condition) {
40+
updatePollingInterval(action.meta.arg, mwApi);
41+
}
42+
if (queryThunk.fulfilled.match(action) || queryThunk.rejected.match(action) && !action.meta.condition) {
43+
startNextPoll(action.meta.arg, mwApi);
44+
}
45+
```
46+
47+
### cacheLifecycle
48+
uses `queryThunk` matching to differentiate between mutation cache and query cache handling
49+
50+
### queryLifecycle
51+
leverages the createAsyncThunk pending/fulfilled/rejected to extend the lifecycle with query specific traits, also uses it to handle onQueryStarted
52+
53+
### batchedActions
54+
55+
### buildMiddleware
56+
57+
`refetchQuery` refires queryThunk with arguments for the `queryThunk` to determine if the query should be sent or not

0 commit comments

Comments
 (0)