Skip to content

Commit

Permalink
adds onQuery and onCacheEntryAdded lifecycles split up middleware…
Browse files Browse the repository at this point in the history
…, add OptionalPromise, add cache entry lifecycle (#1034)

* splits up middleware into multiple files
* adds `onCacheEntryAdded` lifecycle
* adds `onQuery` lifecycle
* adds `OptionalPromise`
* deprecates `onStart`, `onSuccess` and `onError` lifecycle events with "best effort" fallback
* adds `.undo` to `api.util.updateQueryResult` return value
* docs updates
  • Loading branch information
phryneas authored May 13, 2021
1 parent a42a9f2 commit a200459
Show file tree
Hide file tree
Showing 30 changed files with 2,283 additions and 647 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ module.exports = {
// Silence some bizarre "rule not found" TSLint error
'@typescript-eslint/no-angle-bracket-type-assertion': 'off',
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': ['error'],
// Silence some bizarre "rule not found" TSLint error
'@typescript-eslint/no-redeclare': 'off',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': ['error', { functions: false }],
},
Expand Down
83 changes: 55 additions & 28 deletions docs/api/rtk-query/createApi.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const { useGetPokemonByNameQuery } = pokemonApi
`createApi` accepts a single configuration object parameter with the following options:

```ts no-transpile
baseQuery(args: InternalQueryArgs, api: QueryApi): any;
baseQuery(args: InternalQueryArgs, api: BaseQueryApi): any;
endpoints(build: EndpointBuilder<InternalQueryArgs, TagTypes>): Definitions;
tagTypes?: readonly TagTypes[];
reducerPath?: ReducerPath;
Expand Down Expand Up @@ -119,39 +119,66 @@ export const defaultSerializeQueryArgs: SerializeQueryArgs<any> = ({

[summary](docblock://query/endpointDefinitions.ts?token=MutationExtraOptions.invalidatesTags)

- `onStart`, `onError` and `onSuccess` _(optional)_ - Available to both [queries](../../usage/rtk-query/queries.mdx) and [mutations](../../usage/rtk-query/mutations.mdx)
- `onQuery` _(optional)_ - Available to both [queries](../../usage/rtk-query/queries.mdx) and [mutations](../../usage/rtk-query/mutations.mdx)
- Can be used in `mutations` for [optimistic updates](../../usage/rtk-query/optimistic-updates.mdx).
- ```ts title="Mutation lifecycle signatures" no-transpile
function onStart(
- ```ts title="Mutation onQuery signature" no-transpile
async function onQuery(
arg: QueryArg,
mutationApi: MutationApi<ReducerPath, Context>
): void
function onError(
arg: QueryArg,
mutationApi: MutationApi<ReducerPath, Context>,
error: unknown
): void
function onSuccess(
arg: QueryArg,
mutationApi: MutationApi<ReducerPath, Context>,
result: ResultType
): void
{
dispatch,
getState,
extra,
requestId,
resultPromise,
getCacheEntry,
}: MutationLifecycleApi
): Promise<void>
```
- ```ts title="Query lifecycle signatures" no-transpile
function onStart(
- ```ts title="Query onQuery signature" no-transpile
async function onQuery(
arg: QueryArg,
queryApi: QueryApi<ReducerPath, Context>
): void
function onError(
{
dispatch,
getState,
extra,
requestId,
resultPromise,
getCacheEntry,
updateCacheEntry,
}: QueryLifecycleApi
): Promise<void>
```
- `onCacheEntryAdded` _(optional)_ - Available to both [queries](../../usage/rtk-query/queries.mdx) and [mutations](../../usage/rtk-query/mutations.mdx)

- Can be used for TODO
- ```ts title="Mutation onCacheEntryAdded signature" no-transpile
async function onCacheEntryAdded(
arg: QueryArg,
queryApi: QueryApi<ReducerPath, Context>,
error: unknown
): void
function onSuccess(
{
dispatch,
getState,
extra,
requestId,
cleanup,
firstValueResolved,
getCacheEntry,
}: MutationCacheLifecycleApi
): Promise<void>
```
- ```ts title="Query onCacheEntryAdded signature" no-transpile
async function onCacheEntryAdded(
arg: QueryArg,
queryApi: QueryApi<ReducerPath, Context>,
result: ResultType
): void
{
dispatch,
getState,
extra,
requestId,
cleanup,
firstValueResolved,
getCacheEntry,
updateCacheEntry,
}: QueryCacheLifecycleApi
): Promise<void>
```

#### How endpoints get used
Expand Down
91 changes: 54 additions & 37 deletions docs/usage/rtk-query/mutations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,34 @@ const api = createApi({
}),
// Pick out data and prevent nested properties in a hook or selector
transformResponse: (response: { data: Post }) => response.data,
// onStart, onSuccess, onError are useful for optimistic updates
// The 2nd parameter is the destructured `mutationApi`
onStart(
{ id, ...patch },
{ dispatch, getState, extra, requestId, context }
) {},
// `result` is the server response
onSuccess({ id }, mutationApi, result) {},
onError({ id }, { dispatch, getState, extra, requestId, context }) {},
invalidatesTags: ['Post'],
// onQuery is useful for optimistic updates
// The 2nd parameter is the destructured `MutationLifecycleApi`
async onQuery(
arg,
{ dispatch, getState, resultPromise, requestId, extra, getCacheEntry }
) {},
// The 2nd parameter is the destructured `MutationCacheLifecycleApi`
async onCacheEntryAdded(
arg,
{
dispatch,
getState,
extra,
requestId,
cleanup,
firstValueResolved,
getCacheEntry,
}
) {},
// highlight-end
}),
}),
})
```

:::info
Notice the `onStart`, `onSuccess`, `onError` methods? Be sure to check out how they can be used for [optimistic updates](./optimistic-updates)
Notice the `onQuery` method? Be sure to check out how it can be used for [optimistic updates](./optimistic-updates)
:::

### Type interfaces
Expand All @@ -72,29 +82,29 @@ export type MutationDefinition<
type: DefinitionType.mutation
invalidatesTags?: ResultDescription<TagTypes, ResultType, QueryArg>
providesTags?: never
onStart?(arg: QueryArg, mutationApi: MutationApi<ReducerPath, Context>): void
onError?(
onQuery?(
arg: QueryArg,
mutationApi: MutationApi<ReducerPath, Context>,
error: unknown,
meta: BaseQueryMeta<BaseQuery>
): void
onSuccess?(
{
dispatch,
getState,
resultPromise,
requestId,
extra,
getCacheEntry,
}: MutationLifecycleApi
): Promise<void>
onCacheEntryAdded?(
arg: QueryArg,
mutationApi: MutationApi<ReducerPath, Context>,
result: ResultType,
meta: BaseQueryMeta<BaseQuery> | undefined
): void
}
```
```ts title="MutationApi" no-transpile
export interface MutationApi<ReducerPath extends string, Context extends {}> {
dispatch: ThunkDispatch<any, any, AnyAction>
getState(): RootState<any, any, ReducerPath>
extra: unknown
requestId: string
context: Context
{
dispatch,
getState,
extra,
requestId,
cleanup,
firstValueResolved,
getCacheEntry,
}: MutationCacheLifecycleApi
): Promise<void>
}
```
Expand Down Expand Up @@ -171,7 +181,7 @@ export const postApi = createApi({
result
? // successful query
[
...result.map(({ id }) => ({ type: 'Posts', id }) as const),
...result.map(({ id }) => ({ type: 'Posts', id } as const)),
{ type: 'Posts', id: 'LIST' },
]
: // an error occurred, but we still want to refetch this query when `{ type: 'Posts', id: 'LIST' }` is invalidated
Expand Down Expand Up @@ -228,9 +238,16 @@ export const {
} = postApi
```

<iframe src="https://codesandbox.io/embed/concepts-mutations-4d98s?fontsize=14&hidenavigation=1&module=%2Fsrc%2Fapp%2Fservices%2Fposts.ts&theme=dark"
style={{ width: '100%', height: '600px', border: 0, borderRadius: '4px', overflow: 'hidden' }}
title="RTK Query - Mutations Concept"
allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb"
sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"
<iframe
src="https://codesandbox.io/embed/concepts-mutations-4d98s?fontsize=14&hidenavigation=1&module=%2Fsrc%2Fapp%2Fservices%2Fposts.ts&theme=dark"
style={{
width: '100%',
height: '600px',
border: 0,
borderRadius: '4px',
overflow: 'hidden',
}}
title="RTK Query - Mutations Concept"
allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb"
sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"
></iframe>
46 changes: 34 additions & 12 deletions docs/usage/rtk-query/optimistic-updates.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ When you're performing an update on some data that _already exists_ in the cache

The core concepts are:

- in the `onStart` phase of a mutation, you manually set the cached data via `updateQueryResult`
- then, in `onError`, you roll it back via `patchQueryResult`. You don't have to worry about the `onSuccess` lifecycle here.
- when you start a query or mutation, `onQuery` will be executed
- you manually update the cached data by dispatching `api.util.updateQueryResult`
- then, in the case that `promiseResult` rejects, you roll it back via the `.undo` property of the object you got back from the earlier dispatch.

```ts title="Optimistic update mutation example"
```ts title="Optimistic update mutation example (async await)"
// file: types.ts noEmit
export interface Post {
id: number
Expand All @@ -41,24 +42,45 @@ const api = createApi({
method: 'PATCH',
body: patch,
}),
onStart({ id, ...patch }, { dispatch, context }) {
// When we start the request, just immediately update the cache
context.undoPost = dispatch(
invalidatesTags: ['Post'],
// highlight-start
async onQuery({ id, ...patch }, { dispatch, resultPromise }) {
const patchResult = dispatch(
api.util.updateQueryResult('getPost', id, (draft) => {
Object.assign(draft, patch)
})
).inversePatches
},
onError({ id }, { dispatch, context }) {
// If there is an error, roll it back
dispatch(api.util.patchQueryResult('getPost', id, context.undoPost))
)
try {
await resultPromise
} catch {
patchResult.undo()
}
},
invalidatesTags: ['Post'],
// highlight-end
}),
}),
})
```

or, if you prefer the slighty shorter version with `.catch`

```diff
- async onQuery({ id, ...patch }, { dispatch, resultPromise }) {
+ onQuery({ id, ...patch }, { dispatch, resultPromise }) {
const patchResult = dispatch(
api.util.updateQueryResult('getPost', id, (draft) => {
Object.assign(draft, patch)
})
)
- try {
- await resultPromise
- } catch {
- patchResult.undo()
- }
+ resultPromise.catch(patchResult.undo)
}
```

### Example

[View Example](./examples#react-optimistic-updates)
32 changes: 27 additions & 5 deletions docs/usage/rtk-query/queries.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,34 @@ const api = createApi({
query: (id) => ({ url: `post/${id}` }),
// Pick out data and prevent nested properties in a hook or selector
transformResponse: (response: { data: Post }) => response.data,
// The 2nd parameter is the destructured `queryApi`
onStart(id, { dispatch, getState, extra, requestId, context }) {},
// `result` is the server response
onSuccess(id, queryApi, result) {},
onError(id, queryApi) {},
providesTags: (result, error, id) => [{ type: 'Post', id }],
// The 2nd parameter is the destructured `QueryLifecycleApi`
async onQuery(
arg,
{
dispatch,
getState,
extra,
requestId,
resultPromise,
getCacheEntry,
updateCacheEntry,
}
) {},
// The 2nd parameter is the destructured `QueryCacheLifecycleApi`
async onCacheEntryAdded(
arg,
{
dispatch,
getState,
extra,
requestId,
cleanup,
firstValueResolved,
getCacheEntry,
updateCacheEntry,
}
) {},
// highlight-end
}),
}),
Expand Down
18 changes: 0 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a200459

Please sign in to comment.