diff --git a/docs/ConnectedFetch.md b/docs/ConnectedFetch.md index a1edff4..ed3f859 100644 --- a/docs/ConnectedFetch.md +++ b/docs/ConnectedFetch.md @@ -18,6 +18,7 @@ const App = () => ( api="https://my-app.com/api/v1" headers={{ Cache-Control: 'public' }} loader={

♻️ Loading…

} + // onIntercept={() => interceptor} /* Uncomment to manually set `onIntercept` value (see #Props.onIntercept) */ timeout={5000} /* Any unresolved request will automatically be aborted after 5s */ // store={{ networkState: 'online' }} /* Uncomment to manually set `store` value (see #Props.store) */ > @@ -56,6 +57,12 @@ A mandatory prop used to render your app. Basically, you'll just have to wrap yo A component (or a function returning one) render every time a `` is loading data. The typical use case for this prop would be when you have a common loader for your whole app. Instead of using ``'s `loader` prop, you'll just have to do it once here and that's it: you'll have the same beautiful spinner in your whole app! +### onIntercept + +**Type: `InterceptedData => ?RequestToApi`** + +Called when the request resolves with an error, right before rejecting the failing response (provided to [`onError`](Fetch.md#onerror)). See [Interceptor](Fetch.md#interceptor) for more details. + ### store **Type: `Object`** @@ -72,7 +79,7 @@ Value in ms after which you'll want the library to abort any request you'll send ### Duplicated props -You'll notice that `` and `` have a few props in common: `headers`, `loader`, `timeout`. You could ask then: "OK, but what if I use a `loader` in ``, but want a special one in a specific `` ?". Well, it would be a very good question! +You'll notice that `` and `` have a few props in common: `headers`, `loader`, `onIntercept`, `timeout`. You could ask then: "OK, but what if I use a `loader` in ``, but want a special one in a specific `` ?". Well, it would be a very good question! !>React Data Fetching will always apply the prop coming from a `` over the same one coming from ``. diff --git a/docs/Fetch.md b/docs/Fetch.md index 853dc67..0455b2d 100644 --- a/docs/Fetch.md +++ b/docs/Fetch.md @@ -38,7 +38,7 @@ export default class MyComponent extends Component { } /** * You could also do something here to change isRefetchingData - * or isLoadingMore from your state is you want to use `refetch`. + * or isLoadingMore from your state is you want to use `refetchKey`. * You can see an example here: https://goo.gl/wq8iMU. */ } @@ -136,6 +136,13 @@ Called when the response has been received. But beware: seeing this function bei The tradeoff is that you can't use `onFetch` to render a component, see `children`, `component` or `render` to do so. Nothing stops you from using `component`, `render` or `children` to render your component, plus `onFetch` to save your data in the same `` for instance. +### onIntercept + +**Type: `InterceptedData => ?RequestToApi`** + +Called when the request resolves with an error, right before rejecting the failing response (provided to [`onError`](Fetch.md#onerror)). See the [Interceptor](Fetch.md#interceptor) section below for more details. + + ### onLoad **Type: `Function`** @@ -166,11 +173,11 @@ Works exactly like `body`, but is used whenever you need to pass parameters to y Only available if you've configured `` in your app, and provided an `api` (see [`` docs](ConnectedFetch.md#path) for more details). Then, `path` allows you to write your URL in a more convenient way. Instead of writing `url="https://my-app.com/api/v1/news/latest"`, given that `` propagates your `api` URL `https://my-app.com/api/v1` inside every `` instances, you can just write `path="/news/latest"`, and React Data Fetching will automatically construct the corresponding URL. -### refetch +### refetchKey **Type: `any`** -Follows the same principle as [`extraData`](https://facebook.github.io/react-native/docs/flatlist.html#extradata) from React Native's FlatList component. This prop tells `` to re-render. This can be used inside a pull-to-refresh function, or to implement a pagination system for instance. You can pass `any` value here, the only requirement for it to operate as expected is to make sure that the value you passed will change over time. Otherwise, there will be no re-render. +If `refetchKey` changes, then React Data Fetching will fetch again. Similar to how in React, if the `key` changes, then the component gets unmounted & remounted. This can be used inside a pull-to-refresh function, or to implement a pagination system for instance. You can pass `any` value here, the only requirement for it to operate as expected is to make sure that the value you passed will change over time. Otherwise, there will be no refetching. ### render @@ -228,6 +235,106 @@ type Error = { width="900" /> +### Interceptor + +```js +export type InterceptedData = { + currentParams: RequestToApi, + request: XMLHttpRequest, + status: number, +} + +type Interceptor = InterceptedData => ?RequestToApi +``` + +`Object` provided to `onIntercept` prop when the request resolves when an error. Thus, if `onIntercept` is defined, it will be called right before React Data Fetching rejects the response (which will throw an error and fire `onError`). + +This means that `onIntercept` is the perfect place to catch and handle any error received from your API, before `` & `` consume the response. `onIntercept` receives as an argument an `InterceptedData` and lets you play with it. However it expects a `return`-ed value: +- It can be a `RequestToApi` object, which will let React Data Fetching know that you want to make a new request based on this new object. Thus, the previous failing request will be automatically aborted and no error will be thrown. +- You can also return `null`, which means that you don't want to make any new request. By returning so, React Data Fetching will then throw the received response and fire `onError` if you provided it. + + +One of the most common use case for this will be tokens refreshing. Let's say you have an `accessToken` with a 30-minute lifetime and make a call when your `accessToken` isn't valid anymore: we don't want your UX to be deteriorated because of that! So here's how you could handle this case by refreshing your `accessToken` thanks to `onIntercept` and your `refreshToken`: + +```jsx +/* @flow */ + +import React, { Component } from 'react' +import { + Fetch, + requestToApi, + type InterceptedData, + type RequestToApi, +} from 'react-data-fetching' + +import Root from './src' + +type Tokens = { + accessToken: string, + refreshToken: string, +} + +type Props = { + tokens: Tokens, + saveNewTokens: Tokens => void, +} + +class App extends Component { + // ... + _onIntercept = async (data: InterceptedData): ?RequestToApi => { + const { saveNewTokens, tokens } = this.props + let output: ?RequestToApi = null + + if (data && data.status === 401) { + try { + const newTokens = await requestToApi({ + url: 'https://mysite.com/api/v1/auth/refreshToken', + method: 'POST', + body: { refreshToken: tokens.refreshToken }, + }) + if (newTokens) { + saveNewTokens(newTokens) + /** Given that `InterceptedData` provides the `currentParams` + * initially passed to `requestToApi()`, we can simply reuse + * these params and cherry-pick those we want to change + */ + output = { + ...data.currentParams, + body: { + ...data.currentParams.body, + accessToken: newTokens.accessToken, + } + } + } + } + catch (error) { + // Handle the error + } + } + /** As stated by the types, the output will either be + * `null`, so RDF won't do anything, or a `RequestToApi` + * object, thus RDF will just have to directly make a new + * call with the new config provided by `output` and send the + * final result as a `ReturnedData` to the props that need it. + */ + return output + } + + render() { + + } +} + +// ... + +``` + +For the record: this is the 1st feature requested and implemented by a member of the community! 😃🍾 The full story behind it is [right there](https://github.com/CharlesMangwa/react-data-fetching/issues/8)! + + ### Method diff --git a/modules/ConnectedFetch.js b/modules/ConnectedFetch.js index 08a7de9..909ab06 100644 --- a/modules/ConnectedFetch.js +++ b/modules/ConnectedFetch.js @@ -5,9 +5,9 @@ import PropTypes from 'prop-types' import invariant from 'invariant' import { + type Interceptor, type ProviderProps, type Store, - type Interceptor, storeShape, } from './types' @@ -36,9 +36,9 @@ const createConnectedFetch = (): Class<*> => { children: PropTypes.element.isRequired, headers: PropTypes.object, loader: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), + onIntercept: PropTypes.func, store: storeShape, timeout: PropTypes.number, - onIntercept: PropTypes.func, } static contextTypes = { @@ -88,6 +88,10 @@ const createConnectedFetch = (): Class<*> => { this.rdfLoader === nextProps.loader, ' does not support changing `loader` on the fly.', ) + invariant( + this.rdfInterceptor === nextProps.onIntercept, + ' does not support changing `onIntercept` on the fly.', + ) invariant( this.rdfStore === nextProps.store, ' does not support changing `store` on the fly.', diff --git a/modules/types.js b/modules/types.js index ac7ac1a..89b7f7c 100644 --- a/modules/types.js +++ b/modules/types.js @@ -5,7 +5,8 @@ import PropTypes from 'prop-types' // FLOW -export type Interceptor = (InterceptedData) => ?RequestToApi; +export type Interceptor = InterceptedData => ?RequestToApi + export type Context = { rdfApi: string, rdfHeaders: Object, @@ -70,10 +71,10 @@ export type Props = { method: Method, onError?: (?ReturnedData | Error) => void, onFetch?: (?ReturnedData | Error) => void, + onIntercept?: Interceptor, onLoad?: Function, - onProgress?: (Progress) => void, + onProgress?: Progress => void, onTimeout?: Function, - onIntercept?: Interceptor, params?: Object, path?: string, refetch?: any, @@ -88,9 +89,9 @@ export type RequestToApi = { body?: Object, headers?: Object, method: Method, - onProgress?: (Progress) => void, - onTimeout?: Function, + onProgress?: Progress => void, onIntercept?: ?Interceptor, + onTimeout?: Function, params?: Object, url: string, timeout?: number, @@ -99,7 +100,7 @@ export type RequestToApi = { export type InterceptedData = { currentParams: RequestToApi, request: XMLHttpRequest, - status: number + status: number, } export type Store = {