Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Implement mapExchange (replacing errorExchange) #2846

Merged
merged 9 commits into from
Dec 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/violet-eyes-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@urql/core': minor
---

Implement `mapExchange`, which replaces `errorExchange`, allowing `onOperation` and `onResult` to be called to either react to or replace operations and results. For backwards compatibility, this exchange is also exported as `errorExchange` and supports `onError`.
22 changes: 12 additions & 10 deletions docs/advanced/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,10 +325,11 @@ actions, it's a good idea for us to return `false` when `!authState` applies.

[Read more about `@urql/exchange-auth`'s API in our API docs.](../api/auth-exchange.md)

## Handling Logout with the Error Exchange
## Handling Logout by reacting to Errors

We can also handle authentication errors in an `errorExchange` instead of the `authExchange`. To do this, we'll need to add the
`errorExchange` to the exchanges array, _before_ the `authExchange`. The order is very important here:
We can also handle authentication errors in a `mapExchange` instead of the `authExchange`.
To do this, we'll need to add the `mapExchange` to the exchanges array, _before_ the `authExchange`.
The order is very important here:

```js
import { createClient, dedupExchange, cacheExchange, fetchExchange, errorExchange } from 'urql';
Expand All @@ -339,10 +340,9 @@ const client = createClient({
exchanges: [
dedupExchange,
cacheExchange,
errorExchange({
onError: error => {
mapExchange({
onError(error, _operation) {
const isAuthError = error.graphQLErrors.some(e => e.extensions?.code === 'FORBIDDEN');

if (isAuthError) {
logout();
}
Expand All @@ -356,10 +356,12 @@ const client = createClient({
});
```

The `errorExchange` will only receive an auth error when the auth exchange has already tried and failed to handle it. This means we have
either failed to refresh the token, or there is no token refresh functionality. If we receive an auth error in the `errorExchange` (as defined in
the `didAuthError` configuration section above), then we can be confident that it is an auth error that the `authExchange` isn't able to recover
from, and the user should be logged out.
The `mapExchange` will only receive an auth error when the auth exchange has already tried and failed
to handle it. This means we have either failed to refresh the token, or there is no token refresh
functionality. If we receive an auth error in the `mapExchange`'s `onError` function
(as defined in the `didAuthError` configuration section above), then we can be confident that it is
an authentication error that the `authExchange` isn't able to recover from, and the user should be
logged out.

## Cache Invalidation on Logout

Expand Down
64 changes: 58 additions & 6 deletions docs/api/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,9 @@ the `fetchExchange`.
An exchange that writes incoming `Operation`s to `console.log` and
writes completed `OperationResult`s to `console.log`.

This exchange is disabled in production and is based on the `mapExchange`.
If you'd like to customise it, you can replace it with a custom `mapExchange`.

### dedupExchange

An exchange that keeps track of ongoing `Operation`s that haven't returned had
Expand All @@ -334,19 +337,68 @@ and is still waiting for a result.
The `fetchExchange` of type `Exchange` is responsible for sending operations of type `'query'` and
`'mutation'` to a GraphQL API using `fetch`.

### errorExchange
### mapExchange

An exchange that lets you inspect errors. This can be useful for logging, or reacting to
different types of errors (e.g. logging the user out in case of a permission error).
The `mapExchange` allows you to:

- react to or replace operations with `onOperation`,
- react to or replace results with `onResult`,
- and; react to errors in results with `onError`.

It can therefore be used to quickly react to the core events in the `Client` without writing a custom
exchange, effectively allowing you to ship your own `debugExchange`.

```ts
mapExchange({
onOperation(operation) {
console.log('operation', operation);
},
onResult(result) {
console.log('result', result);
},
});
```

It can also be used to react only to errors, which is the same as checking for `result.error`:

```ts
errorExchange({
onError: (error: CombinedError, operation: Operation) => {
console.log('An error!', error);
mapExchange({
onError(error, operation) {
console.log(`The operation ${operation.key} has errored with:`, error);
},
});
```

Lastly, it can be used to map operations and results, which may be useful to update the
`OperationContext` or perform other standard tasks that require you to wait for a result:

```ts
import { mapExchange, makeOperation } from '@urql/core';

mapExchange({
async onOperation(operation) {
// NOTE: This is only for illustration purposes
return makeOperation(operation.kind, operation, {
...operation.context,
test: true,
});
},
async onResult(result) {
// NOTE: This is only for illustration purposes
if (result.data === undefined) result.data = null;
return result;
},
});
```

### errorExchange (deprecated)

An exchange that lets you inspect errors. This can be useful for logging, or reacting to
different types of errors (e.g. logging the user out in case of a permission error).

In newer versions of `@urql/core`, it's identical to the `mapExchange` and its export has been
replaced as the `mapExchange` also allows you to pass an `onError` function.

## Utilities

### gql
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ sending requests to our API.

Some of the exchanges that are available to us are:

- [`errorExchange`](./api/core.md#errorexchange): Allows a global callback to be called when any error occurs
- [`mapExchange`](./api/core.md#mapexchange): Allows reacting to operations, results, and errors
- [`ssrExchange`](./advanced/server-side-rendering.md): Allows for a server-side renderer to
collect results for client-side rehydration.
- [`retryExchange`](./advanced/retry-operations.md): Allows operations to be retried
Expand Down
119 changes: 0 additions & 119 deletions packages/core/src/exchanges/error.test.ts

This file was deleted.

18 changes: 0 additions & 18 deletions packages/core/src/exchanges/error.ts

This file was deleted.

10 changes: 9 additions & 1 deletion packages/core/src/exchanges/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ export { dedupExchange } from './dedup';
export { fetchExchange } from './fetch';
export { fallbackExchangeIO } from './fallback';
export { composeExchanges } from './compose';
export { errorExchange } from './error';

export type {
SubscriptionOperation,
SubscriptionForwarder,
SubscriptionExchangeOpts,
} from './subscription';

export { mapExchange, mapExchange as errorExchange } from './map';
export type { MapExchangeOpts } from './map';

import { cacheExchange } from './cache';
import { dedupExchange } from './dedup';
Expand Down
Loading