Skip to content

Commit

Permalink
docs(*): add onIntercept documentation 📰
Browse files Browse the repository at this point in the history
  • Loading branch information
CharlesMangwa committed Apr 14, 2018
1 parent 09ee29c commit 568b04a
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 12 deletions.
9 changes: 8 additions & 1 deletion docs/ConnectedFetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const App = () => (
api="https://my-app.com/api/v1"
headers={{ Cache-Control: 'public' }}
loader={<p>♻️ Loading…</p>}
// 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) */
>
Expand Down Expand Up @@ -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 `<Fetch>` 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 `<Fetch>`'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`**
Expand All @@ -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 `<ConnectedFetch>` and `<Fetch>` have a few props in common: `headers`, `loader`, `timeout`. You could ask then: "OK, but what if I use a `loader` in `<ConnectedFetch>`, but want a special one in a specific `<Fetch>` ?". Well, it would be a very good question!
You'll notice that `<ConnectedFetch>` and `<Fetch>` have a few props in common: `headers`, `loader`, `onIntercept`, `timeout`. You could ask then: "OK, but what if I use a `loader` in `<ConnectedFetch>`, but want a special one in a specific `<Fetch>` ?". Well, it would be a very good question!

!>React Data Fetching will always apply the prop coming from a `<Fetch>` over the same one coming from `<ConnectedFetch>`.

Expand Down
113 changes: 110 additions & 3 deletions docs/Fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
}
Expand Down Expand Up @@ -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 `<Fetch>` 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`**
Expand Down Expand Up @@ -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 `<ConnectedFetch>` in your app, and provided an `api` (see [`<ConnectedFetch>` 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 `<ConnectedFetch>` propagates your `api` URL `https://my-app.com/api/v1` inside every `<Fetch>` 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 `<Fetch>` 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

Expand Down Expand Up @@ -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 `<ConnectedFetch>` & `<Fetch>` 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<Props> {
// ...
_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() {
<Fetch
// ...
onIntercept={this._onIntercept}
/>
}
}

// ...

```
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
Expand Down
8 changes: 6 additions & 2 deletions modules/ConnectedFetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -88,6 +88,10 @@ const createConnectedFetch = (): Class<*> => {
this.rdfLoader === nextProps.loader,
'<ConnectedFetch> does not support changing `loader` on the fly.',
)
invariant(
this.rdfInterceptor === nextProps.onIntercept,
'<ConnectedFetch> does not support changing `onIntercept` on the fly.',
)
invariant(
this.rdfStore === nextProps.store,
'<ConnectedFetch> does not support changing `store` on the fly.',
Expand Down
13 changes: 7 additions & 6 deletions modules/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -99,7 +100,7 @@ export type RequestToApi = {
export type InterceptedData = {
currentParams: RequestToApi,
request: XMLHttpRequest,
status: number
status: number,
}

export type Store = {
Expand Down

0 comments on commit 568b04a

Please sign in to comment.