Skip to content

Commit 5fe391e

Browse files
committed
Merge branch 'master' into create-slice-creators
2 parents a84abbd + ff65194 commit 5fe391e

File tree

59 files changed

+1640
-468
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1640
-468
lines changed

.codesandbox/ci.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
"sandboxes": [
33
"vanilla",
44
"vanilla-ts",
5-
"github/reduxjs/rtk-github-issues-example",
65
"/examples/query/react/basic",
76
"/examples/query/react/advanced",
87
"/examples/action-listener/counter",

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ jobs:
106106
fail-fast: false
107107
matrix:
108108
node: ['20.x']
109-
ts: ['4.7', '4.8', '4.9', '5.0', '5.1', '5.2', '5.3']
109+
ts: ['4.7', '4.8', '4.9', '5.0', '5.1', '5.2', '5.3', '5.4']
110110
steps:
111111
- name: Checkout repo
112112
uses: actions/checkout@v4
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# invalidationByTags
2+
3+
## Overview
4+
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+
9+
```ts no-transpile
10+
const isThunkActionWithTags = isAnyOf(
11+
isFulfilled(mutationThunk),
12+
isRejectedWithValue(mutationThunk),
13+
)
14+
15+
const isQueryEnd = isAnyOf(
16+
isFulfilled(mutationThunk, queryThunk),
17+
isRejected(mutationThunk, queryThunk),
18+
)
19+
```
20+
21+
## Triggers
22+
23+
The handler has 3 core conditionals that trigger a sequence:
24+
25+
_Conditional 1 AND 3 are identical in process except the tags are calculated from the payload rather than from the action and endpointDefinition_
26+
27+
1. Mutation trigger
28+
2. Query trigger
29+
3. Manual invalidation via `api.util.invalidateTags` trigger
30+
31+
```ts no-transpile
32+
const handler: ApiMiddlewareInternalHandler = (action, mwApi) => {
33+
if (isThunkActionWithTags(action)) {
34+
invalidateTags(
35+
calculateProvidedByThunk(
36+
action,
37+
'invalidatesTags',
38+
endpointDefinitions,
39+
assertTagType,
40+
),
41+
mwApi,
42+
)
43+
} else if (isQueryEnd(action)) {
44+
invalidateTags([], mwApi)
45+
} else if (api.util.invalidateTags.match(action)) {
46+
invalidateTags(
47+
calculateProvidedBy(
48+
action.payload,
49+
undefined,
50+
undefined,
51+
undefined,
52+
undefined,
53+
assertTagType,
54+
),
55+
mwApi,
56+
)
57+
}
58+
}
59+
```
60+
61+
## Core Sequence
62+
63+
1. `invalidateTags()` initiates:
64+
1. invalidateTags function is called with a list of tags generated from the action metadata
65+
2. in the case of a [queryThunk] resolution an empty set of tags is always provided
66+
2. The tags calculated are added to the list of pending tags to invalidate (see [delayed](#Delayed))
67+
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`
68+
4. Pending tags are reset to an empty list, if there are no tags the function ends here
69+
5. Selects all `{ endpointName, originalArgs, queryCacheKey }` combinations that would be invalidated by a specific set of tags.
70+
6. Iterates through queryCacheKeys selected and performs one of two actions if the query exists\*
71+
1. removes cached query result - via the `removeQueryResult` action - if no subscription is active
72+
2. if the query is "uninitialized" it initiates a `refetchQuery` action
73+
74+
```js no-transpile
75+
const toInvalidate = api.util.selectInvalidatedBy(rootState, tags)
76+
context.batch(() => {
77+
const valuesArray = Array.from(toInvalidate.values())
78+
for (const { queryCacheKey } of valuesArray) {
79+
const querySubState = state.queries[queryCacheKey]
80+
const subscriptionSubState =
81+
internalState.currentSubscriptions[queryCacheKey] ?? {}
82+
if (querySubState) {
83+
if (countObjectKeys(subscriptionSubState) === 0) {
84+
mwApi.dispatch(
85+
removeQueryResult({
86+
queryCacheKey,
87+
}),
88+
)
89+
} else if (querySubState.status !== 'uninitialized' /* uninitialized */) {
90+
mwApi.dispatch(refetchQuery(querySubState, queryCacheKey))
91+
}
92+
}
93+
}
94+
})
95+
```
96+
97+
:::note
98+
Step 6 is performed within a `context.batch()` call.
99+
:::
100+
101+
### Delayed
102+
103+
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.
104+
105+
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.
106+
107+
```ts no-transpile
108+
function invalidateTags(
109+
newTags: readonly FullTagDescription<string>[],
110+
mwApi: SubMiddlewareApi,
111+
) {
112+
const rootState = mwApi.getState()
113+
const state = rootState[reducerPath]
114+
115+
pendingTagInvalidations.push(...newTags)
116+
117+
if (
118+
state.config.invalidationBehavior === 'delayed' &&
119+
hasPendingRequests(state)
120+
) {
121+
return
122+
}
123+
```
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# BuildSlice
2+
3+
## Slices
4+
5+
### querySlice
6+
7+
#### reducers
8+
9+
- `removeQueryResult` - delete a specific cacheKey's stored result
10+
- `queryResultPatched` - patch a specific cacheKey's result
11+
12+
#### extraReducers - matching queryThunk cases
13+
14+
- `queryThunk.pending`
15+
- Initially sets QueryStatus to uninitialized
16+
- updates QueryStatus to pending
17+
- Generates requestId
18+
- stores originalArgs
19+
- stores startedTimeStamp
20+
- `queryThunk.fulfilled`
21+
- handles merge functionality first
22+
- otherwise updates the cache data, creates a fulfilledTimeStamp and deletes the substates error
23+
24+
```ts no-transpile
25+
if (merge) {
26+
if (substate.data !== undefined) {
27+
const { fulfilledTimeStamp, arg, baseQueryMeta, requestId } = meta
28+
// There's existing cache data. Let the user merge it in themselves.
29+
// We're already inside an Immer-powered reducer, and the user could just mutate `substate.data`
30+
// themselves inside of `merge()`. But, they might also want to return a new value.
31+
// Try to let Immer figure that part out, save the result, and assign it to `substate.data`.
32+
let newData = createNextState(substate.data, (draftSubstateData) => {
33+
// As usual with Immer, you can mutate _or_ return inside here, but not both
34+
return merge(draftSubstateData, payload, {
35+
arg: arg.originalArgs,
36+
baseQueryMeta,
37+
fulfilledTimeStamp,
38+
requestId,
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+
57+
#### reducers
58+
59+
- `removeMutationResult`
60+
- calls `getMutationCacheKey` from payload
61+
- if cacheKey is in draft it deletes `draft[cacheKey`(?)
62+
63+
#### extraReducers - matching mutationThunk cases
64+
65+
- `mutationThunk.pending`
66+
- exits if track is set to false
67+
- otherwise updates appropriate cacheKey with requestId, pending status and startedTimeStamp
68+
- `mutationThunk.fulfilled`
69+
- exits if track is set to false
70+
- otherwise sets data off payload and fulfilledTimeStamp
71+
- `mutationThunk.rejected`
72+
- exits if track is set to false
73+
- otherwise sets error and status to rejected
74+
- `hasRehydrationInfo`
75+
- iterates through and resets entries for all fulfilled or rejected status
76+
77+
### invalidationSlice
78+
79+
#### reducers
80+
81+
- updateProvidedBy
82+
- takes queryCacheKey and providedTags from payload
83+
- appends to a list of idSubscriptions the queryCacheKey that are currently subscribed to for each tag
84+
85+
#### extraReducers
86+
87+
- `querySlice.actions.removeQueryResult`,
88+
- deletes relevant queryCacheKey entry from list of subscription ids
89+
- `hasRehydrationInfo`
90+
- TODO
91+
- `queryThunk.fulfilled` or `queryThunk.rejected`
92+
- gets list of tags from action and endpoint definition
93+
- gets queryCacheKey
94+
- calls updateProvidedBy action
95+
96+
### subscriptionSlice / internalSubscriptionSlice
97+
98+
#### reducers
99+
100+
- updateSubscriptionOptions
101+
- unsubscribeQueryResult
102+
- internal_getRTKQSubscriptions
103+
- subscriptionsUpdated
104+
- applyPatches() to the state from the payload
105+
106+
### configSlice
107+
108+
#### reducers
109+
110+
- middlewareRegistered
111+
- toggles whether the middleware is registered or if there is a conflict
112+
113+
#### extraReducers
114+
115+
- `onOnline`
116+
- manages state.online in response to listenerMiddleware
117+
- `onOffline`
118+
- manages state.online in response to listenerMiddleware
119+
- `onFocus`
120+
- manages state.focused in response to listenerMiddleware
121+
- `onFocusLost`
122+
- manages state.focused in response to listenerMiddleware
123+
- `hasRehydrationInfo`
124+
- 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`"
125+
126+
## Functions
127+
128+
### `updateQuerySubstateIfExists`
129+
130+
Utility function that takes the api/endpoint state, queryCacheKey and Update function.
131+
The "SubState" is determined by accessing the `queryCacheKey` value inside the state. If the substate exists, the update function is executed on the substate.
132+
133+
```js no-transpile
134+
function updateQuerySubstateIfExists(state, queryCacheKey, update) {
135+
const substate = state[queryCacheKey]
136+
if (substate) {
137+
update(substate)
138+
}
139+
}
140+
```
141+
142+
### `getMutationCacheKey`
143+
144+
conditionally determines the cachekey to be used for the mutation, prioritising the argument provided, followed by the provided cacheKey, and the generated requestId otherwise
145+
146+
```ts no-transpile
147+
export function getMutationCacheKey(
148+
id:
149+
| { fixedCacheKey?: string; requestId?: string }
150+
| MutationSubstateIdentifier
151+
| { requestId: string; arg: { fixedCacheKey?: string | undefined } },
152+
): string | undefined {
153+
return ('arg' in id ? id.arg.fixedCacheKey : id.fixedCacheKey) ?? id.requestId
154+
}
155+
```
156+
157+
### `getMutationSubstateIfExists`
158+
159+
same as query version except it uses the id instead of the queryCacheKey, and uses the `getMutationCacheKey` to determine the cachekey
160+
161+
```js no-transpile
162+
function updateMutationSubstateIfExists(state, id, update) {
163+
const substate = state[getMutationCacheKey(id)]
164+
if (substate) {
165+
update(substate)
166+
}
167+
}
168+
```

docs/rtk-query/internal/overview.mdx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
11+
When `createApi()` is called it takes the options provided and calls internally the `buildCreateApi()` function passing into it two modules:
12+
13+
_Modules are RTK-Query's method of customizing how the `createApi` method handles endpoints._
14+
15+
- `coreModule()` - responsible for the majority of the internal handling using core redux logic i.e. slices, reducers, asyncThunks.
16+
- `reactHooksModule()` - a module that generates react hooks from endpoints using react-redux
17+
18+
## Core Module
19+
20+
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.
21+
22+
### buildThunks
23+
24+
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:
25+
26+
- `queryThunk`
27+
- `mutationThunk`
28+
- `patchedQueryData`
29+
- `updateQueryData`
30+
- `upsertQueryData`
31+
- `prefetch`
32+
- `buildMatchThunkActions`
33+
34+
### buildSlice
35+
36+
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.
37+
38+
The slices built inside this "build" are:
39+
_Some of which have their own actions_
40+
41+
- querySlice
42+
- mutationSlice
43+
- invalidationSlice
44+
- subscriptionSlice (used as a dummy slice to generate actions internally)
45+
- internalSubscriptionsSlice
46+
- configSlice (internal tracking of focus state, online state, hydration etc)
47+
48+
buildSlice also exposes the core action `resetApiState` which is subsequently added to the `api.util`
49+
50+
### buildMiddleware
51+
52+
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.
53+
54+
Each middleware built during this step is referred to internally as a "Handler" and are as follows:
55+
56+
- `buildDevCheckHandler
57+
- `buildCacheCollectionHandler
58+
- `buildInvalidationByTagsHandler
59+
- `buildPollingHandler
60+
- `buildCacheLifecycleHandler
61+
- `buildQueryLifecycleHandler
62+
63+
### buildSelectors
64+
65+
build selectors is a crucial step that exposes to the `api` and utils:
66+
67+
- `buildQuerySelector
68+
- `buildMutationSelector
69+
- `selectInvalidatedBy
70+
- `selectCachedArgsForQuery
71+
72+
### return
73+
74+
Finally each endpoint passed into the `createApi()` is iterated over and assigned either the query or the mutation selectors, initiators and match cases.

0 commit comments

Comments
 (0)