-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into create-slice-creators
- Loading branch information
Showing
59 changed files
with
1,640 additions
and
468 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
docs/rtk-query/internal/buildMiddleware/invalidationByTags.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
# invalidationByTags | ||
|
||
## Overview | ||
|
||
`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. | ||
|
||
The matchers used for a "invalidation sequence" are these two cases: | ||
|
||
```ts no-transpile | ||
const isThunkActionWithTags = isAnyOf( | ||
isFulfilled(mutationThunk), | ||
isRejectedWithValue(mutationThunk), | ||
) | ||
|
||
const isQueryEnd = isAnyOf( | ||
isFulfilled(mutationThunk, queryThunk), | ||
isRejected(mutationThunk, queryThunk), | ||
) | ||
``` | ||
|
||
## Triggers | ||
|
||
The handler has 3 core conditionals that trigger a sequence: | ||
|
||
_Conditional 1 AND 3 are identical in process except the tags are calculated from the payload rather than from the action and endpointDefinition_ | ||
|
||
1. Mutation trigger | ||
2. Query trigger | ||
3. Manual invalidation via `api.util.invalidateTags` trigger | ||
|
||
```ts no-transpile | ||
const handler: ApiMiddlewareInternalHandler = (action, mwApi) => { | ||
if (isThunkActionWithTags(action)) { | ||
invalidateTags( | ||
calculateProvidedByThunk( | ||
action, | ||
'invalidatesTags', | ||
endpointDefinitions, | ||
assertTagType, | ||
), | ||
mwApi, | ||
) | ||
} else if (isQueryEnd(action)) { | ||
invalidateTags([], mwApi) | ||
} else if (api.util.invalidateTags.match(action)) { | ||
invalidateTags( | ||
calculateProvidedBy( | ||
action.payload, | ||
undefined, | ||
undefined, | ||
undefined, | ||
undefined, | ||
assertTagType, | ||
), | ||
mwApi, | ||
) | ||
} | ||
} | ||
``` | ||
|
||
## Core Sequence | ||
|
||
1. `invalidateTags()` initiates: | ||
1. invalidateTags function is called with a list of tags generated from the action metadata | ||
2. in the case of a [queryThunk] resolution an empty set of tags is always provided | ||
2. The tags calculated are added to the list of pending tags to invalidate (see [delayed](#Delayed)) | ||
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` | ||
4. Pending tags are reset to an empty list, if there are no tags the function ends here | ||
5. Selects all `{ endpointName, originalArgs, queryCacheKey }` combinations that would be invalidated by a specific set of tags. | ||
6. Iterates through queryCacheKeys selected and performs one of two actions if the query exists\* | ||
1. removes cached query result - via the `removeQueryResult` action - if no subscription is active | ||
2. if the query is "uninitialized" it initiates a `refetchQuery` action | ||
|
||
```js no-transpile | ||
const toInvalidate = api.util.selectInvalidatedBy(rootState, tags) | ||
context.batch(() => { | ||
const valuesArray = Array.from(toInvalidate.values()) | ||
for (const { queryCacheKey } of valuesArray) { | ||
const querySubState = state.queries[queryCacheKey] | ||
const subscriptionSubState = | ||
internalState.currentSubscriptions[queryCacheKey] ?? {} | ||
if (querySubState) { | ||
if (countObjectKeys(subscriptionSubState) === 0) { | ||
mwApi.dispatch( | ||
removeQueryResult({ | ||
queryCacheKey, | ||
}), | ||
) | ||
} else if (querySubState.status !== 'uninitialized' /* uninitialized */) { | ||
mwApi.dispatch(refetchQuery(querySubState, queryCacheKey)) | ||
} | ||
} | ||
} | ||
}) | ||
``` | ||
:::note | ||
Step 6 is performed within a `context.batch()` call. | ||
::: | ||
### Delayed | ||
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. | ||
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. | ||
```ts no-transpile | ||
function invalidateTags( | ||
newTags: readonly FullTagDescription<string>[], | ||
mwApi: SubMiddlewareApi, | ||
) { | ||
const rootState = mwApi.getState() | ||
const state = rootState[reducerPath] | ||
|
||
pendingTagInvalidations.push(...newTags) | ||
|
||
if ( | ||
state.config.invalidationBehavior === 'delayed' && | ||
hasPendingRequests(state) | ||
) { | ||
return | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
# BuildSlice | ||
|
||
## Slices | ||
|
||
### querySlice | ||
|
||
#### reducers | ||
|
||
- `removeQueryResult` - delete a specific cacheKey's stored result | ||
- `queryResultPatched` - patch a specific cacheKey's result | ||
|
||
#### extraReducers - matching queryThunk cases | ||
|
||
- `queryThunk.pending` | ||
- Initially sets QueryStatus to uninitialized | ||
- updates QueryStatus to pending | ||
- Generates requestId | ||
- stores originalArgs | ||
- stores startedTimeStamp | ||
- `queryThunk.fulfilled` | ||
- handles merge functionality first | ||
- otherwise updates the cache data, creates a fulfilledTimeStamp and deletes the substates error | ||
|
||
```ts no-transpile | ||
if (merge) { | ||
if (substate.data !== undefined) { | ||
const { fulfilledTimeStamp, arg, baseQueryMeta, requestId } = meta | ||
// There's existing cache data. Let the user merge it in themselves. | ||
// We're already inside an Immer-powered reducer, and the user could just mutate `substate.data` | ||
// themselves inside of `merge()`. But, they might also want to return a new value. | ||
// Try to let Immer figure that part out, save the result, and assign it to `substate.data`. | ||
let newData = createNextState(substate.data, (draftSubstateData) => { | ||
// As usual with Immer, you can mutate _or_ return inside here, but not both | ||
return merge(draftSubstateData, payload, { | ||
arg: arg.originalArgs, | ||
baseQueryMeta, | ||
fulfilledTimeStamp, | ||
requestId, | ||
}) | ||
}) | ||
substate.data = newData | ||
} else { | ||
// Presumably a fresh request. Just cache the response data. | ||
substate.data = payload | ||
} | ||
} | ||
``` | ||
|
||
- `queryThunk.rejected` | ||
- utilises `condition()` from `queryThunk` and does nothing if the rejection is a result of `condition()` (indicates a thunk is already running here) | ||
- else substate.error is set and the status is changed to rejected | ||
- `hasRehydrationInfo` | ||
- iterates through and resets entries for all fulfilled or rejected status | ||
|
||
### mutationSlice | ||
|
||
#### reducers | ||
|
||
- `removeMutationResult` | ||
- calls `getMutationCacheKey` from payload | ||
- if cacheKey is in draft it deletes `draft[cacheKey`(?) | ||
|
||
#### extraReducers - matching mutationThunk cases | ||
|
||
- `mutationThunk.pending` | ||
- exits if track is set to false | ||
- otherwise updates appropriate cacheKey with requestId, pending status and startedTimeStamp | ||
- `mutationThunk.fulfilled` | ||
- exits if track is set to false | ||
- otherwise sets data off payload and fulfilledTimeStamp | ||
- `mutationThunk.rejected` | ||
- exits if track is set to false | ||
- otherwise sets error and status to rejected | ||
- `hasRehydrationInfo` | ||
- iterates through and resets entries for all fulfilled or rejected status | ||
|
||
### invalidationSlice | ||
|
||
#### reducers | ||
|
||
- updateProvidedBy | ||
- takes queryCacheKey and providedTags from payload | ||
- appends to a list of idSubscriptions the queryCacheKey that are currently subscribed to for each tag | ||
|
||
#### extraReducers | ||
|
||
- `querySlice.actions.removeQueryResult`, | ||
- deletes relevant queryCacheKey entry from list of subscription ids | ||
- `hasRehydrationInfo` | ||
- TODO | ||
- `queryThunk.fulfilled` or `queryThunk.rejected` | ||
- gets list of tags from action and endpoint definition | ||
- gets queryCacheKey | ||
- calls updateProvidedBy action | ||
|
||
### subscriptionSlice / internalSubscriptionSlice | ||
|
||
#### reducers | ||
|
||
- updateSubscriptionOptions | ||
- unsubscribeQueryResult | ||
- internal_getRTKQSubscriptions | ||
- subscriptionsUpdated | ||
- applyPatches() to the state from the payload | ||
|
||
### configSlice | ||
|
||
#### reducers | ||
|
||
- middlewareRegistered | ||
- toggles whether the middleware is registered or if there is a conflict | ||
|
||
#### extraReducers | ||
|
||
- `onOnline` | ||
- manages state.online in response to listenerMiddleware | ||
- `onOffline` | ||
- manages state.online in response to listenerMiddleware | ||
- `onFocus` | ||
- manages state.focused in response to listenerMiddleware | ||
- `onFocusLost` | ||
- manages state.focused in response to listenerMiddleware | ||
- `hasRehydrationInfo` | ||
- 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`" | ||
|
||
## Functions | ||
|
||
### `updateQuerySubstateIfExists` | ||
|
||
Utility function that takes the api/endpoint state, queryCacheKey and Update function. | ||
The "SubState" is determined by accessing the `queryCacheKey` value inside the state. If the substate exists, the update function is executed on the substate. | ||
|
||
```js no-transpile | ||
function updateQuerySubstateIfExists(state, queryCacheKey, update) { | ||
const substate = state[queryCacheKey] | ||
if (substate) { | ||
update(substate) | ||
} | ||
} | ||
``` | ||
|
||
### `getMutationCacheKey` | ||
|
||
conditionally determines the cachekey to be used for the mutation, prioritising the argument provided, followed by the provided cacheKey, and the generated requestId otherwise | ||
|
||
```ts no-transpile | ||
export function getMutationCacheKey( | ||
id: | ||
| { fixedCacheKey?: string; requestId?: string } | ||
| MutationSubstateIdentifier | ||
| { requestId: string; arg: { fixedCacheKey?: string | undefined } }, | ||
): string | undefined { | ||
return ('arg' in id ? id.arg.fixedCacheKey : id.fixedCacheKey) ?? id.requestId | ||
} | ||
``` | ||
|
||
### `getMutationSubstateIfExists` | ||
|
||
same as query version except it uses the id instead of the queryCacheKey, and uses the `getMutationCacheKey` to determine the cachekey | ||
|
||
```js no-transpile | ||
function updateMutationSubstateIfExists(state, id, update) { | ||
const substate = state[getMutationCacheKey(id)] | ||
if (substate) { | ||
update(substate) | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
# RTKQ internal | ||
|
||
## Overview | ||
|
||
> 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. | ||
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. | ||
|
||
## createApi - The Entry Point | ||
|
||
When `createApi()` is called it takes the options provided and calls internally the `buildCreateApi()` function passing into it two modules: | ||
|
||
_Modules are RTK-Query's method of customizing how the `createApi` method handles endpoints._ | ||
|
||
- `coreModule()` - responsible for the majority of the internal handling using core redux logic i.e. slices, reducers, asyncThunks. | ||
- `reactHooksModule()` - a module that generates react hooks from endpoints using react-redux | ||
|
||
## Core Module | ||
|
||
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. | ||
|
||
### buildThunks | ||
|
||
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: | ||
|
||
- `queryThunk` | ||
- `mutationThunk` | ||
- `patchedQueryData` | ||
- `updateQueryData` | ||
- `upsertQueryData` | ||
- `prefetch` | ||
- `buildMatchThunkActions` | ||
|
||
### buildSlice | ||
|
||
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. | ||
|
||
The slices built inside this "build" are: | ||
_Some of which have their own actions_ | ||
|
||
- querySlice | ||
- mutationSlice | ||
- invalidationSlice | ||
- subscriptionSlice (used as a dummy slice to generate actions internally) | ||
- internalSubscriptionsSlice | ||
- configSlice (internal tracking of focus state, online state, hydration etc) | ||
|
||
buildSlice also exposes the core action `resetApiState` which is subsequently added to the `api.util` | ||
|
||
### buildMiddleware | ||
|
||
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. | ||
|
||
Each middleware built during this step is referred to internally as a "Handler" and are as follows: | ||
|
||
- `buildDevCheckHandler | ||
- `buildCacheCollectionHandler | ||
- `buildInvalidationByTagsHandler | ||
- `buildPollingHandler | ||
- `buildCacheLifecycleHandler | ||
- `buildQueryLifecycleHandler | ||
|
||
### buildSelectors | ||
|
||
build selectors is a crucial step that exposes to the `api` and utils: | ||
|
||
- `buildQuerySelector | ||
- `buildMutationSelector | ||
- `selectInvalidatedBy | ||
- `selectCachedArgsForQuery | ||
|
||
### return | ||
|
||
Finally each endpoint passed into the `createApi()` is iterated over and assigned either the query or the mutation selectors, initiators and match cases. |
Oops, something went wrong.