Skip to content

Commit

Permalink
Merge pull request #1841 from dreyks/feature/rtkq-transform-error-res…
Browse files Browse the repository at this point in the history
…ponse
  • Loading branch information
markerikson authored Aug 16, 2022
2 parents 3ca3c88 + 48f52e0 commit a5e6587
Show file tree
Hide file tree
Showing 12 changed files with 189 additions and 20 deletions.
29 changes: 29 additions & 0 deletions docs/rtk-query/api/createApi.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ export type QueryDefinition<
arg: QueryArg
): ResultType | Promise<ResultType>

/* transformErrorResponse only available with `query`, not `queryFn` */
transformErrorResponse?(
baseQueryReturnValue: BaseQueryError<BaseQuery>,
meta: BaseQueryMeta<BaseQuery>,
arg: QueryArg
): unknown

extraOptions?: BaseQueryExtraOptions<BaseQuery>

providesTags?: ResultDescription<
Expand Down Expand Up @@ -226,6 +233,13 @@ export type MutationDefinition<
arg: QueryArg
): ResultType | Promise<ResultType>

/* transformErrorResponse only available with `query`, not `queryFn` */
transformErrorResponse?(
baseQueryReturnValue: BaseQueryError<BaseQuery>,
meta: BaseQueryMeta<BaseQuery>,
arg: QueryArg
): unknown

extraOptions?: BaseQueryExtraOptions<BaseQuery>

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

### `transformErrorResponse`

_(optional, not applicable with `queryFn`)_

[summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.transformErrorResponse)

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`.

See also [Customizing query responses with `transformErrorResponse`](../usage/customizing-queries.mdx#customizing-query-responses-with-transformerrorresponse)

```ts title="Unpack a deeply nested error object" no-transpile
transformErrorResponse: (response, meta, arg) =>
response.data.some.deeply.nested.errorObject
```

### `extraOptions`

_(optional)_
Expand Down
2 changes: 1 addition & 1 deletion docs/rtk-query/usage-with-typescript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ The `BaseQueryFn` type accepts the following generics:
- `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).
- `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.
- `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.
- `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).
- `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).
:::note
Expand Down
51 changes: 51 additions & 0 deletions docs/rtk-query/usage/customizing-queries.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,57 @@ transformResponse: (response) =>

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).

## Customizing query responses with `transformErrorResponse`

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.

`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.

By default, the payload from the server is returned directly.

```ts
function defaultTransformResponse(
baseQueryReturnValue: unknown,
meta: unknown,
arg: unknown
) {
return baseQueryReturnValue
}
```

To change it, provide a function that looks like:

```ts title="Unpack a deeply nested error object" no-transpile
transformErrorResponse: (response, meta, arg) =>
response.data.some.deeply.nested.errorObject
```

`transformErrorResponse` is called with the `meta` property returned from the `baseQuery` as its second
argument, which can be used while determining the transformed response. The value for `meta` is
dependent on the `baseQuery` used.

```ts title="transformErrorResponse meta example" no-transpile
transformErrorResponse: (response: { data: { sideA: Tracks; sideB: Tracks } }, meta, arg) => {
if (meta?.coinFlip === 'heads') {
return response.data.sideA
}
return response.data.sideB
}
```

`transformErrorResponse` is called with the `arg` property provided to the endpoint as its third
argument, which can be used while determining the transformed response. The value for `arg` is
dependent on the `endpoint` used, as well as the argument used when calling the query/mutation.

```ts title="transformErrorResponse arg example" no-transpile
transformErrorResponse: (response: Posts, meta, arg) => {
return {
originalArg: arg,
error: response,
}
}
```

## Customizing queries with `queryFn`

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.
Expand Down
2 changes: 2 additions & 0 deletions docs/rtk-query/usage/mutations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const api = createApi({
}),
// Pick out data and prevent nested properties in a hook or selector
transformResponse: (response: { data: Post }, meta, arg) => response.data,
// Pick out errors and prevent nested properties in a hook or selector
transformErrorResponse: (response: { status: string | number }, meta, arg) => response.status,
invalidatesTags: ['Post'],
// onQueryStarted is useful for optimistic updates
// The 2nd parameter is the destructured `MutationLifecycleApi`
Expand Down
2 changes: 2 additions & 0 deletions docs/rtk-query/usage/queries.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ const api = createApi({
query: (id) => ({ url: `post/${id}` }),
// Pick out data and prevent nested properties in a hook or selector
transformResponse: (response: { data: Post }, meta, arg) => response.data,
// Pick out errors and prevent nested properties in a hook or selector
transformErrorResponse: (response: { status: string | number }, meta, arg) => response.status,
providesTags: (result, error, id) => [{ type: 'Post', id }],
// The 2nd parameter is the destructured `QueryLifecycleApi`
async onQueryStarted(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ declare module '../../endpointDefinitions' {
error: unknown
meta?: undefined
/**
* 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.
* 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.
* There can not be made any assumption about the shape of `error`.
*/
isUnhandledError: true
Expand Down
36 changes: 30 additions & 6 deletions packages/toolkit/src/query/core/buildThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,22 +353,46 @@ export function buildThunks<
}
)
} catch (error) {
if (error instanceof HandledError) {
return rejectWithValue(error.value, { baseQueryMeta: error.meta })
let catchedError = error
if (catchedError instanceof HandledError) {
let transformErrorResponse: (
baseQueryReturnValue: any,
meta: any,
arg: any
) => any = defaultTransformResponse

if (
endpointDefinition.query &&
endpointDefinition.transformErrorResponse
) {
transformErrorResponse = endpointDefinition.transformErrorResponse
}
try {
return rejectWithValue(
await transformErrorResponse(
catchedError.value,
catchedError.meta,
arg.originalArgs
),
{ baseQueryMeta: catchedError.meta }
)
} catch (e) {
catchedError = e
}
}
if (
typeof process !== 'undefined' &&
process.env.NODE_ENV === 'development'
process.env.NODE_ENV !== 'production'
) {
console.error(
`An unhandled error occurred processing a request for the endpoint "${arg.endpointName}".
In the case of an unhandled error, no tags will be "provided" or "invalidated".`,
error
catchedError
)
} else {
console.error(error)
console.error(catchedError)
}
throw error
throw catchedError
}
}

Expand Down
13 changes: 13 additions & 0 deletions packages/toolkit/src/query/endpointDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ interface EndpointDefinitionWithQuery<
meta: BaseQueryMeta<BaseQuery>,
arg: QueryArg
): ResultType | Promise<ResultType>
/**
* A function to manipulate the data returned by a failed query or mutation.
*/
transformErrorResponse?(
baseQueryReturnValue: BaseQueryError<BaseQuery>,
meta: BaseQueryMeta<BaseQuery>,
arg: QueryArg
): unknown
/**
* Defaults to `true`.
*
Expand Down Expand Up @@ -130,6 +138,7 @@ interface EndpointDefinitionWithQueryFn<
): MaybePromise<QueryReturnValue<ResultType, BaseQueryError<BaseQuery>>>
query?: never
transformResponse?: never
transformErrorResponse?: never
/**
* Defaults to `true`.
*
Expand Down Expand Up @@ -425,6 +434,8 @@ export type EndpointBuilder<
* query: (id) => ({ url: `post/${id}` }),
* // Pick out data and prevent nested properties in a hook or selector
* transformResponse: (response) => response.data,
* // Pick out error and prevent nested properties in a hook or selector
* transformErrorResponse: (response) => response.error,
* // `result` is the server response
* providesTags: (result, error, id) => [{ type: 'Post', id }],
* // trigger side effects or optimistic updates
Expand Down Expand Up @@ -455,6 +466,8 @@ export type EndpointBuilder<
* query: ({ id, ...patch }) => ({ url: `post/${id}`, method: 'PATCH', body: patch }),
* // Pick out data and prevent nested properties in a hook or selector
* transformResponse: (response) => response.data,
* // Pick out error and prevent nested properties in a hook or selector
* transformErrorResponse: (response) => response.error,
* // `result` is the server response
* invalidatesTags: (result, error, id) => [{ type: 'Post', id }],
* // trigger side effects or optimistic updates
Expand Down
9 changes: 0 additions & 9 deletions packages/toolkit/src/query/tests/cleanup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,6 @@ test('Minimizes the number of subscription dispatches when multiple components a
return <>{listItems}</>
}

const start = Date.now()

render(<ParentComponent />, {
wrapper: storeRef.wrapper,
})
Expand All @@ -189,10 +187,6 @@ test('Minimizes the number of subscription dispatches when multiple components a
return screen.getAllByText(/42/).length > 0
})

const end = Date.now()

const timeElapsed = end - start

const subscriptions = getSubscriptionsA()

expect(Object.keys(subscriptions!).length).toBe(NUM_LIST_ITEMS)
Expand All @@ -203,7 +197,4 @@ test('Minimizes the number of subscription dispatches when multiple components a
// 'api/executeQuery/fulfilled'
// ]
expect(actionTypes.length).toBe(4)
// Could be flaky in CI, but we'll see.
// Currently seeing 1000ms in local dev, 6300 without the batching fixes
expect(timeElapsed).toBeLessThan(2500)
}, 25000)
19 changes: 19 additions & 0 deletions packages/toolkit/src/query/tests/createApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ describe('endpoint definition typings', () => {
describe('additional transformResponse behaviors', () => {
type SuccessResponse = { value: 'success' }
type EchoResponseData = { banana: 'bread' }
type ErrorResponse = { value: 'error' }
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
endpoints: (build) => ({
Expand All @@ -531,6 +532,16 @@ describe('additional transformResponse behaviors', () => {
transformResponse: (response: { body: { nested: EchoResponseData } }) =>
response.body.nested,
}),
mutationWithError: build.mutation({
query: () => ({
url: '/error',
method: 'POST',
}),
transformErrorResponse: (response) => {
const data = response.data as ErrorResponse
return data.value
},
}),
mutationWithMeta: build.mutation({
query: () => ({
url: '/echo',
Expand Down Expand Up @@ -596,6 +607,14 @@ describe('additional transformResponse behaviors', () => {
expect('data' in result && result.data).toEqual({ banana: 'bread' })
})

test('transformResponse transforms a response from a mutation with an error', async () => {
const result = await storeRef.store.dispatch(
api.endpoints.mutationWithError.initiate({})
)

expect('error' in result && result.error).toEqual('error')
})

test('transformResponse can inject baseQuery meta into the end result from a mutation', async () => {
const result = await storeRef.store.dispatch(
api.endpoints.mutationWithMeta.initiate({})
Expand Down
28 changes: 28 additions & 0 deletions packages/toolkit/src/query/tests/devWarnings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,34 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".
In the case of an unhandled error, no tags will be "provided" or "invalidated". [Error: this was kinda expected]`)
})

test('error thrown in `transformErrorResponse`', async () => {
const api = createApi({
baseQuery() {
return { error: {} }
},
endpoints: (build) => ({
// @ts-ignore TS doesn't like `() => never` for `tER`
transformErRspn: build.query<number, void>({
// @ts-ignore TS doesn't like `() => never` for `tER`
query: () => '/dummy',
// @ts-ignore TS doesn't like `() => never` for `tER`
transformErrorResponse() {
throw new Error('this was kinda expected')
},
}),
}),
})
const store = configureStore({
reducer: { [api.reducerPath]: api.reducer },
middleware: (gdm) => gdm().concat(api.middleware),
})
await store.dispatch(api.endpoints.transformErRspn.initiate())

expect(getLog().log)
.toBe(`An unhandled error occurred processing a request for the endpoint "transformErRspn".
In the case of an unhandled error, no tags will be "provided" or "invalidated". [Error: this was kinda expected]`)
})

test('`fetchBaseQuery`: error thrown in `prepareHeaders`', async () => {
const api = createApi({
baseQuery: fetchBaseQuery({
Expand Down
16 changes: 13 additions & 3 deletions packages/toolkit/src/query/tests/queryFn.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import type { QuerySubState } from '@reduxjs/toolkit/dist/query/core/apiState'

describe('queryFn base implementation tests', () => {
const baseQuery: BaseQueryFn<string, { wrappedByBaseQuery: string }, string> =
jest.fn((arg: string) => ({
data: { wrappedByBaseQuery: arg },
}))
jest.fn((arg: string) => arg.includes('withErrorQuery')
? ({ error: `cut${arg}` })
: ({ data: { wrappedByBaseQuery: arg } }))

const api = createApi({
baseQuery,
Expand All @@ -24,6 +24,14 @@ describe('queryFn base implementation tests', () => {
return response.wrappedByBaseQuery
},
}),
withErrorQuery: build.query<string, string>({
query(arg: string) {
return `resultFrom(${arg})`
},
transformErrorResponse(response) {
return response.slice(3)
},
}),
withQueryFn: build.query<string, string>({
queryFn(arg: string) {
return { data: `resultFrom(${arg})` }
Expand Down Expand Up @@ -141,6 +149,7 @@ describe('queryFn base implementation tests', () => {

const {
withQuery,
withErrorQuery,
withQueryFn,
withErrorQueryFn,
withThrowingQueryFn,
Expand All @@ -166,6 +175,7 @@ describe('queryFn base implementation tests', () => {

test.each([
['withQuery', withQuery, 'data'],
['withErrorQuery', withErrorQuery, 'error'],
['withQueryFn', withQueryFn, 'data'],
['withErrorQueryFn', withErrorQueryFn, 'error'],
['withThrowingQueryFn', withThrowingQueryFn, 'throw'],
Expand Down

0 comments on commit a5e6587

Please sign in to comment.