Skip to content

Commit

Permalink
Merge branches 'master', 'master' and 'feature/4252-entity-adapter-so…
Browse files Browse the repository at this point in the history
…rting' of https://github.com/reduxjs/redux-toolkit into feature/4252-entity-adapter-sorting
  • Loading branch information
codesandbot committed Apr 18, 2024
2 parents d0b3ba5 + e46eb99 commit 36e15d4
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 87 deletions.
51 changes: 28 additions & 23 deletions docs/rtk-query/internal/buildMiddleware/invalidationByTags.mdx
@@ -1,10 +1,11 @@
# 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),
Expand All @@ -21,11 +22,12 @@ const isQueryEnd = isAnyOf(

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*
_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)) {
Expand Down Expand Up @@ -56,38 +58,40 @@ const handler: ApiMiddlewareInternalHandler = (action, 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) )
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
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);
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] ?? {};
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));
mwApi.dispatch(
removeQueryResult({
queryCacheKey,
}),
)
} else if (querySubState.status !== 'uninitialized' /* uninitialized */) {
mwApi.dispatch(refetchQuery(querySubState, queryCacheKey))
}
}
}
});
})
```
:::note
Expand All @@ -99,6 +103,7 @@ Step 6 is performed within a `context.batch()` call.
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>[],
Expand All @@ -115,4 +120,4 @@ function invalidateTags(
) {
return
}
```
```
127 changes: 72 additions & 55 deletions docs/rtk-query/internal/buildSlice.mdx
Expand Up @@ -3,41 +3,41 @@
## Slices

### querySlice

#### reducers
- `removeQueryResult` - delete a specific cacheKey's stored result
- `queryResultPatched` - patch a specific cacheKey's result

- `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
- 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
- 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
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,
})
},
)
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.
Expand All @@ -47,85 +47,102 @@ let newData = createNextState(
```

- `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
- 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
- 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`(?)
- 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
- 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
- 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
- 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
- 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
- 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
- deletes relevant queryCacheKey entry from list of subscription ids
- `hasRehydrationInfo`
- TODO
- TODO
- `queryThunk.fulfilled` or `queryThunk.rejected`
- gets list of tags from action and endpoint definition
- gets queryCacheKey
- calls updateProvidedBy action
- 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
- applyPatches() to the state from the payload

### configSlice

#### reducers

- middlewareRegistered
- toggles whether the middleware is registered or if there is a conflict
- toggles whether the middleware is registered or if there is a conflict

#### extraReducers

- `onOnline`
- manages state.online in response to listenerMiddleware
- manages state.online in response to listenerMiddleware
- `onOffline`
- manages state.online in response to listenerMiddleware
- manages state.online in response to listenerMiddleware
- `onFocus`
- manages state.focused in response to listenerMiddleware
- manages state.focused in response to listenerMiddleware
- `onFocusLost`
- manages state.focused in response to listenerMiddleware
- 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`"

- 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];
const substate = state[queryCacheKey]
if (substate) {
update(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:
Expand All @@ -140,12 +157,12 @@ export function getMutationCacheKey(
### `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)];
const substate = state[getMutationCacheKey(id)]
if (substate) {
update(substate);
update(substate)
}
}
```

11 changes: 9 additions & 2 deletions docs/rtk-query/internal/overview.mdx
Expand Up @@ -7,9 +7,10 @@
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.*
_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
Expand All @@ -19,6 +20,7 @@ When `createApi()` is called it takes the options provided and calls internally
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`
Expand All @@ -30,10 +32,12 @@ RTK-Query's internal functionality operates using the same `asyncThunk` exposed
- `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*
_Some of which have their own actions_

- querySlice
- mutationSlice
- invalidationSlice
Expand All @@ -44,6 +48,7 @@ The slices built inside this "build" are:
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:
Expand All @@ -56,6 +61,7 @@ Each middleware built during this step is referred to internally as a "Handler"
- `buildQueryLifecycleHandler

### buildSelectors

build selectors is a crucial step that exposes to the `api` and utils:

- `buildQuerySelector
Expand All @@ -64,4 +70,5 @@ build selectors is a crucial step that exposes to the `api` and utils:
- `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.

0 comments on commit 36e15d4

Please sign in to comment.