Skip to content

Commit a5e6587

Browse files
authored
Merge pull request #1841 from dreyks/feature/rtkq-transform-error-response
2 parents 3ca3c88 + 48f52e0 commit a5e6587

File tree

12 files changed

+189
-20
lines changed

12 files changed

+189
-20
lines changed

docs/rtk-query/api/createApi.mdx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@ export type QueryDefinition<
158158
arg: QueryArg
159159
): ResultType | Promise<ResultType>
160160

161+
/* transformErrorResponse only available with `query`, not `queryFn` */
162+
transformErrorResponse?(
163+
baseQueryReturnValue: BaseQueryError<BaseQuery>,
164+
meta: BaseQueryMeta<BaseQuery>,
165+
arg: QueryArg
166+
): unknown
167+
161168
extraOptions?: BaseQueryExtraOptions<BaseQuery>
162169

163170
providesTags?: ResultDescription<
@@ -226,6 +233,13 @@ export type MutationDefinition<
226233
arg: QueryArg
227234
): ResultType | Promise<ResultType>
228235

236+
/* transformErrorResponse only available with `query`, not `queryFn` */
237+
transformErrorResponse?(
238+
baseQueryReturnValue: BaseQueryError<BaseQuery>,
239+
meta: BaseQueryMeta<BaseQuery>,
240+
arg: QueryArg
241+
): unknown
242+
229243
extraOptions?: BaseQueryExtraOptions<BaseQuery>
230244

231245
invalidatesTags?: ResultDescription<TagTypes, ResultType, QueryArg>
@@ -431,6 +445,21 @@ transformResponse: (response, meta, arg) =>
431445
response.some.deeply.nested.collection
432446
```
433447

448+
### `transformErrorResponse`
449+
450+
_(optional, not applicable with `queryFn`)_
451+
452+
[summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.transformErrorResponse)
453+
454+
In some cases, you may want to manipulate the error returned from a query before you put it in the cache. In this instance, you can take advantage of `transformErrorResponse`.
455+
456+
See also [Customizing query responses with `transformErrorResponse`](../usage/customizing-queries.mdx#customizing-query-responses-with-transformerrorresponse)
457+
458+
```ts title="Unpack a deeply nested error object" no-transpile
459+
transformErrorResponse: (response, meta, arg) =>
460+
response.data.some.deeply.nested.errorObject
461+
```
462+
434463
### `extraOptions`
435464

436465
_(optional)_

docs/rtk-query/usage-with-typescript.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ The `BaseQueryFn` type accepts the following generics:
135135
- `Result` - The type to be returned in the `data` property for the success case. Unless you expect all queries and mutations to return the same type, it is recommended to keep this typed as `unknown`, and specify the types individually as shown [below](#typing-query-and-mutation-endpoints).
136136
- `Error` - The type to be returned for the `error` property in the error case. This type also applies to all [`queryFn`](#typing-a-queryfn) functions used in endpoints throughout the API definition.
137137
- `DefinitionExtraOptions` - The type for the third parameter of the function. The value provided to the [`extraOptions`](./api/createApi.mdx#extraoptions) property on an endpoint will be passed here.
138-
- `Meta` - the type of the `meta` property that may be returned from calling the `baseQuery`. The `meta` property is accessible as the second argument to [`transformResponse`](./api/createApi.mdx#transformresponse).
138+
- `Meta` - the type of the `meta` property that may be returned from calling the `baseQuery`. The `meta` property is accessible as the second argument to [`transformResponse`](./api/createApi.mdx#transformresponse) and [`transformErrorResponse`](./api/createApi.mdx#transformerrorresponse).
139139
140140
:::note
141141

docs/rtk-query/usage/customizing-queries.mdx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,57 @@ transformResponse: (response) =>
175175

176176
See also [Websocket Chat API with a transformed response shape](./streaming-updates.mdx#websocket-chat-api-with-a-transformed-response-shape) for an example of `transformResponse` normalizing response data in combination with `createEntityAdapter`, while also updating further data using [`streaming updates`](./streaming-updates.mdx).
177177

178+
## Customizing query responses with `transformErrorResponse`
179+
180+
Individual endpoints on [`createApi`](../api/createApi.mdx) accept a [`transformErrorResponse`](../api/createApi.mdx) property which allows manipulation of the errir returned by a query or mutation before it hits the cache.
181+
182+
`transformErrorResponse` is called with the error that a failed `baseQuery` returns for the corresponding endpoint, and the return value of `transformErrorResponse` is used as the cached error associated with that endpoint call.
183+
184+
By default, the payload from the server is returned directly.
185+
186+
```ts
187+
function defaultTransformResponse(
188+
baseQueryReturnValue: unknown,
189+
meta: unknown,
190+
arg: unknown
191+
) {
192+
return baseQueryReturnValue
193+
}
194+
```
195+
196+
To change it, provide a function that looks like:
197+
198+
```ts title="Unpack a deeply nested error object" no-transpile
199+
transformErrorResponse: (response, meta, arg) =>
200+
response.data.some.deeply.nested.errorObject
201+
```
202+
203+
`transformErrorResponse` is called with the `meta` property returned from the `baseQuery` as its second
204+
argument, which can be used while determining the transformed response. The value for `meta` is
205+
dependent on the `baseQuery` used.
206+
207+
```ts title="transformErrorResponse meta example" no-transpile
208+
transformErrorResponse: (response: { data: { sideA: Tracks; sideB: Tracks } }, meta, arg) => {
209+
if (meta?.coinFlip === 'heads') {
210+
return response.data.sideA
211+
}
212+
return response.data.sideB
213+
}
214+
```
215+
216+
`transformErrorResponse` is called with the `arg` property provided to the endpoint as its third
217+
argument, which can be used while determining the transformed response. The value for `arg` is
218+
dependent on the `endpoint` used, as well as the argument used when calling the query/mutation.
219+
220+
```ts title="transformErrorResponse arg example" no-transpile
221+
transformErrorResponse: (response: Posts, meta, arg) => {
222+
return {
223+
originalArg: arg,
224+
error: response,
225+
}
226+
}
227+
```
228+
178229
## Customizing queries with `queryFn`
179230

180231
Individual endpoints on [`createApi`](../api/createApi.mdx) accept a [`queryFn`](../api/createApi.mdx#queryfn) property which allows a given endpoint to ignore `baseQuery` for that endpoint by providing an inline function determining how that query resolves.

docs/rtk-query/usage/mutations.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ const api = createApi({
5151
}),
5252
// Pick out data and prevent nested properties in a hook or selector
5353
transformResponse: (response: { data: Post }, meta, arg) => response.data,
54+
// Pick out errors and prevent nested properties in a hook or selector
55+
transformErrorResponse: (response: { status: string | number }, meta, arg) => response.status,
5456
invalidatesTags: ['Post'],
5557
// onQueryStarted is useful for optimistic updates
5658
// The 2nd parameter is the destructured `MutationLifecycleApi`

docs/rtk-query/usage/queries.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ const api = createApi({
5858
query: (id) => ({ url: `post/${id}` }),
5959
// Pick out data and prevent nested properties in a hook or selector
6060
transformResponse: (response: { data: Post }, meta, arg) => response.data,
61+
// Pick out errors and prevent nested properties in a hook or selector
62+
transformErrorResponse: (response: { status: string | number }, meta, arg) => response.status,
6163
providesTags: (result, error, id) => [{ type: 'Post', id }],
6264
// The 2nd parameter is the destructured `QueryLifecycleApi`
6365
async onQueryStarted(

packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ declare module '../../endpointDefinitions' {
6060
error: unknown
6161
meta?: undefined
6262
/**
63-
* If this is `true`, that means that this error is the result of `baseQueryFn`, `queryFn` or `transformResponse` throwing an error instead of handling it properly.
63+
* If this is `true`, that means that this error is the result of `baseQueryFn`, `queryFn`, `transformResponse` or `transformErrorResponse` throwing an error instead of handling it properly.
6464
* There can not be made any assumption about the shape of `error`.
6565
*/
6666
isUnhandledError: true

packages/toolkit/src/query/core/buildThunks.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -353,22 +353,46 @@ export function buildThunks<
353353
}
354354
)
355355
} catch (error) {
356-
if (error instanceof HandledError) {
357-
return rejectWithValue(error.value, { baseQueryMeta: error.meta })
356+
let catchedError = error
357+
if (catchedError instanceof HandledError) {
358+
let transformErrorResponse: (
359+
baseQueryReturnValue: any,
360+
meta: any,
361+
arg: any
362+
) => any = defaultTransformResponse
363+
364+
if (
365+
endpointDefinition.query &&
366+
endpointDefinition.transformErrorResponse
367+
) {
368+
transformErrorResponse = endpointDefinition.transformErrorResponse
369+
}
370+
try {
371+
return rejectWithValue(
372+
await transformErrorResponse(
373+
catchedError.value,
374+
catchedError.meta,
375+
arg.originalArgs
376+
),
377+
{ baseQueryMeta: catchedError.meta }
378+
)
379+
} catch (e) {
380+
catchedError = e
381+
}
358382
}
359383
if (
360384
typeof process !== 'undefined' &&
361-
process.env.NODE_ENV === 'development'
385+
process.env.NODE_ENV !== 'production'
362386
) {
363387
console.error(
364388
`An unhandled error occurred processing a request for the endpoint "${arg.endpointName}".
365389
In the case of an unhandled error, no tags will be "provided" or "invalidated".`,
366-
error
390+
catchedError
367391
)
368392
} else {
369-
console.error(error)
393+
console.error(catchedError)
370394
}
371-
throw error
395+
throw catchedError
372396
}
373397
}
374398

packages/toolkit/src/query/endpointDefinitions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ interface EndpointDefinitionWithQuery<
6464
meta: BaseQueryMeta<BaseQuery>,
6565
arg: QueryArg
6666
): ResultType | Promise<ResultType>
67+
/**
68+
* A function to manipulate the data returned by a failed query or mutation.
69+
*/
70+
transformErrorResponse?(
71+
baseQueryReturnValue: BaseQueryError<BaseQuery>,
72+
meta: BaseQueryMeta<BaseQuery>,
73+
arg: QueryArg
74+
): unknown
6775
/**
6876
* Defaults to `true`.
6977
*
@@ -130,6 +138,7 @@ interface EndpointDefinitionWithQueryFn<
130138
): MaybePromise<QueryReturnValue<ResultType, BaseQueryError<BaseQuery>>>
131139
query?: never
132140
transformResponse?: never
141+
transformErrorResponse?: never
133142
/**
134143
* Defaults to `true`.
135144
*
@@ -425,6 +434,8 @@ export type EndpointBuilder<
425434
* query: (id) => ({ url: `post/${id}` }),
426435
* // Pick out data and prevent nested properties in a hook or selector
427436
* transformResponse: (response) => response.data,
437+
* // Pick out error and prevent nested properties in a hook or selector
438+
* transformErrorResponse: (response) => response.error,
428439
* // `result` is the server response
429440
* providesTags: (result, error, id) => [{ type: 'Post', id }],
430441
* // trigger side effects or optimistic updates
@@ -455,6 +466,8 @@ export type EndpointBuilder<
455466
* query: ({ id, ...patch }) => ({ url: `post/${id}`, method: 'PATCH', body: patch }),
456467
* // Pick out data and prevent nested properties in a hook or selector
457468
* transformResponse: (response) => response.data,
469+
* // Pick out error and prevent nested properties in a hook or selector
470+
* transformErrorResponse: (response) => response.error,
458471
* // `result` is the server response
459472
* invalidatesTags: (result, error, id) => [{ type: 'Post', id }],
460473
* // trigger side effects or optimistic updates

packages/toolkit/src/query/tests/cleanup.test.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,6 @@ test('Minimizes the number of subscription dispatches when multiple components a
177177
return <>{listItems}</>
178178
}
179179

180-
const start = Date.now()
181-
182180
render(<ParentComponent />, {
183181
wrapper: storeRef.wrapper,
184182
})
@@ -189,10 +187,6 @@ test('Minimizes the number of subscription dispatches when multiple components a
189187
return screen.getAllByText(/42/).length > 0
190188
})
191189

192-
const end = Date.now()
193-
194-
const timeElapsed = end - start
195-
196190
const subscriptions = getSubscriptionsA()
197191

198192
expect(Object.keys(subscriptions!).length).toBe(NUM_LIST_ITEMS)
@@ -203,7 +197,4 @@ test('Minimizes the number of subscription dispatches when multiple components a
203197
// 'api/executeQuery/fulfilled'
204198
// ]
205199
expect(actionTypes.length).toBe(4)
206-
// Could be flaky in CI, but we'll see.
207-
// Currently seeing 1000ms in local dev, 6300 without the batching fixes
208-
expect(timeElapsed).toBeLessThan(2500)
209200
}, 25000)

packages/toolkit/src/query/tests/createApi.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ describe('endpoint definition typings', () => {
516516
describe('additional transformResponse behaviors', () => {
517517
type SuccessResponse = { value: 'success' }
518518
type EchoResponseData = { banana: 'bread' }
519+
type ErrorResponse = { value: 'error' }
519520
const api = createApi({
520521
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
521522
endpoints: (build) => ({
@@ -531,6 +532,16 @@ describe('additional transformResponse behaviors', () => {
531532
transformResponse: (response: { body: { nested: EchoResponseData } }) =>
532533
response.body.nested,
533534
}),
535+
mutationWithError: build.mutation({
536+
query: () => ({
537+
url: '/error',
538+
method: 'POST',
539+
}),
540+
transformErrorResponse: (response) => {
541+
const data = response.data as ErrorResponse
542+
return data.value
543+
},
544+
}),
534545
mutationWithMeta: build.mutation({
535546
query: () => ({
536547
url: '/echo',
@@ -596,6 +607,14 @@ describe('additional transformResponse behaviors', () => {
596607
expect('data' in result && result.data).toEqual({ banana: 'bread' })
597608
})
598609

610+
test('transformResponse transforms a response from a mutation with an error', async () => {
611+
const result = await storeRef.store.dispatch(
612+
api.endpoints.mutationWithError.initiate({})
613+
)
614+
615+
expect('error' in result && result.error).toEqual('error')
616+
})
617+
599618
test('transformResponse can inject baseQuery meta into the end result from a mutation', async () => {
600619
const result = await storeRef.store.dispatch(
601620
api.endpoints.mutationWithMeta.initiate({})

0 commit comments

Comments
 (0)