diff --git a/README.md b/README.md index 87fbca0..44d099e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This library abstracts those features into a generic HTTP component. ✓ Uses the native [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API ✓ Smart deduping of requests ✓ Powerful and customizable response caching -✓ Compose requests +✓ Support for parallel requests ✓ Polling (coming soon) ✓ Small footprint (~2kb gzipped) @@ -37,15 +37,15 @@ yarn add react-request ### Getting Started -Here's a simple example of using React Request. +Here's a quick look at what using React Request is like: ```js -import { Request } from 'react-request'; +import { Fetch } from 'react-request'; class App extends Component { render() { return ( - { if (fetching) { @@ -69,24 +69,19 @@ class App extends Component { } ``` -> Note: the name given to this library in the above example will shadow the -> [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) -> constructor. Most people do not use the Request constructor directly, but if -> you prefer it you can use another name, such as `Req`, instead. - Need to make multiple requests? We got you. ```js -import { RequestComposer } from 'react-request'; +import { FetchComposer } from 'react-request'; class App extends Component { render() { return ( - , - , - + , + , + ]} render={([postOne, postTwo, postThree]) => { return ( @@ -117,7 +112,175 @@ Check out the API reference below for more. ### API -Documentation coming soon. +This library has two exports: + +* `Fetch`: A component for making a single HTTP request +* `FetchComposer`: A component for making parallel HTTP requests + +#### `` + +A component for making a single HTTP request. It accepts every value of `init` and `input` +from the +[`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) +API as a prop, in addition to a few other things. + +Props from the `fetch()` method are: + +* `url` +* `method`: defaults to `"GET"` +* `body` +* `credentials` +* `headers` +* `mode` +* `cache` +* `redirect` +* `referrer`: defaults to `"about:client"` +* `referrerPolicy`: defaults to `""` +* `integrity`: defaults to `""` +* `keepalive` +* `signal` + +To learn more about the valid options for these props, refer to the +[`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) +documentation. + +Here's an example demonstrating some of the most commonly-used props: + +```jsx + { + ; + }} +/> +``` + +In addition to the `fetch()` props, there are a number of other useful props. + +##### `render` + +The [render prop](https://cdb.reacttraining.com/use-a-render-prop-50de598f11ce) of this component. +It is called with one argument, `result`, an object with the following keys: + +* `fetching`: A Boolean representing whether or not a request is currently in flight for this component +* `error`: A Boolean representing if a network error occurred. Note that HTTP "error" status codes do not + cause `error` to be `true`; only failed or aborted network requests do. +* `response`: An instance of [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). The + `body` will already be read, and made available to you as `response.data`. +* `data`: An alias of `response.data` +* `fetch`: A function that makes the HTTP request. See notes below. +* `requestName`: The name of the request (see `requestName` below) + +There are three common use cases for the `fetch` prop: + +* For GET requests, it can allow users to refresh the data +* Anytime there is a network error, you can use this function to retry the request +* When `lazy` is `true`, you can use this to actually make the request, typically as + a result of user input + +`fetch` accepts one argument: `options`. Any of the `fetch()` options described above are valid +`options`. This allows you to customize the request from within the component. + +##### `lazy` + +Whether or not the request will be called when the component mounts. The default value +is based on the request method that you use. + +| Method | Default value | +| ------------------------ | ------------- | +| GET, HEAD, OPTIONS | `false` | +| POST, PUT, PATCH, DELETE | `true` | + +##### `onResponse` + +A function that is called when a request is received. Receives two arguments: `error` and `response`. + +```jsx + { + if (error) { + console.log('Ruh roh', error); + } else { + console.log('Got a response!', response); + } + }} + render={() => { +
Hello
; + }} +/> +``` + +##### `transformData` + +A function that is called with the data returned from the response. You can use this +hook to transform the data before it is passed into `render`. + +```jsx + { + return data.post; + } + render={({ fetching, error, response, data }) => { +
+ {fetching && ('Loading...')} + {error && ('There was an error.')} + {!fetching && !error && response.status === 200 && ( +
+

{data.title}

+
{data.content}
+
+ )} +
+ }} +/> +``` + +##### `contentType` + +The content type of the response body. Defaults to `json`. Valid values are the methods +on [Body](https://developer.mozilla.org/en-US/docs/Web/API/Body). + +##### `requestName` + +A name to give this request, which can help with debugging purposes. The request name is +analogous to a function name in JavaScript. Although we could use anonymous functions +everywhere, we tend to give them names to help humans read and debug the code. + +```jsx + +``` + +##### `fetchPolicy` + +This determines how the request interacts with the cache. For documentation, refer to the +[Apollo documentation](https://www.apollographql.com/docs/react/basics/queries.html#graphql-config-options-fetchPolicy). +This prop is identical to the Apollo prop. + +(The API will be listed here shortly). + +--- + +#### `` + +A component that simplifies making parallel requests. + +##### `requests` + +An array of `Fetch` components. Use any of the above props, but leave out `render`. + +> Note: if you pass a `render` prop, it will be ignored. + +##### `render` + +A function that is called with the array of responses from `requests`. ### Acknowledgements diff --git a/src/request-composer.js b/src/fetch-composer.js similarity index 90% rename from src/request-composer.js rename to src/fetch-composer.js index 3a060ce..e498cd3 100644 --- a/src/request-composer.js +++ b/src/fetch-composer.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -export default function RequestComposer({ requests = [], render }) { +export default function FetchComposer({ requests = [], render }) { if (typeof render !== 'function') { return null; } @@ -20,7 +20,6 @@ export default function RequestComposer({ requests = [], render }) { return render(responses); } - // This is the index of the Request component within `requests` const requestIndex = childrenRequests.length - 1; const request = requests[requestIndex]; @@ -47,7 +46,7 @@ export default function RequestComposer({ requests = [], render }) { return chainRequests(reversedRequests); } -RequestComposer.propTypes = { +FetchComposer.propTypes = { render: PropTypes.func, requests: PropTypes.array }; diff --git a/src/fetch-dedupe.js b/src/fetch-dedupe.js index 71a9b2e..36b3f7b 100644 --- a/src/fetch-dedupe.js +++ b/src/fetch-dedupe.js @@ -21,7 +21,7 @@ function resolveRequest({ requestKey, res, err }) { requests[requestKey] = null; } -export default function fetchDedupe(input, init, { requestKey, type }) { +export default function fetchDedupe(input, init, { requestKey, contentType }) { if (!requests[requestKey]) { requests[requestKey] = []; } @@ -45,7 +45,7 @@ export default function fetchDedupe(input, init, { requestKey, type }) { // The response body is a ReadableStream. ReadableStreams can only be read a single // time, so we must handle that in a central location, here, before resolving // the fetch. - res[type]().then(data => { + res[contentType]().then(data => { res.data = data; resolveRequest({ requestKey, res }); }); diff --git a/src/request.js b/src/fetch.js similarity index 77% rename from src/request.js rename to src/fetch.js index dc83a63..d9e62f0 100644 --- a/src/request.js +++ b/src/fetch.js @@ -7,11 +7,11 @@ import fetchDedupe from './fetch-dedupe'; // The value of each key is a Response instance const responseCache = {}; -function getRequestKey({ url, method, type, body }) { - return [url, method, type, body].join('||'); +function getRequestKey({ url, method, contentType, body }) { + return [url, method, contentType, body].join('||'); } -export default class Request extends React.Component { +export default class Fetch extends React.Component { render() { const { render, requestName } = this.props; const { fetching, response, data, error } = this.state; @@ -32,23 +32,48 @@ export default class Request extends React.Component { } } - state = { - requestName: this.props.requestName, - fetching: !this.props.lazy, - response: null, - data: null, - error: null + constructor(props, context) { + super(props, context); + + this.state = { + requestName: props.requestName, + fetching: !this.isLazy(), + response: null, + data: null, + error: null + }; + } + + isLazy = props => { + const { lazy, method } = props || this.props; + + const uppercaseMethod = method.toUpperCase(); + + let laziness; + + // We default to being lazy for "write" requests, + // such as POST, PATCH, DELETE, and so on. + if (typeof lazy === 'undefined') { + laziness = + uppercaseMethod !== 'GET' && + uppercaseMethod !== 'HEAD' && + uppercaseMethod !== 'OPTIONS'; + } else { + laziness = lazy; + } + + return laziness; }; componentDidMount() { - if (!this.props.lazy) { + if (!this.isLazy()) { this.fetchData(); } } componentWillReceiveProps(nextProps) { // only refresh when keys with primitive types change - const refreshProps = ['url', 'method', 'type', 'body']; + const refreshProps = ['url', 'method', 'contentType', 'body']; if (refreshProps.some(key => this.props[key] !== nextProps[key])) { this.fetchData(nextProps); } @@ -68,7 +93,7 @@ export default class Request extends React.Component { credentials, headers, method, - type, + contentType, mode, cache, redirect, @@ -79,7 +104,7 @@ export default class Request extends React.Component { signal } = Object.assign({}, this.props, options); - const requestKey = getRequestKey({ url, method, body, type }); + const requestKey = getRequestKey({ url, method, body, contentType }); const onResponseReceived = ({ error, response }) => { if (this.willUnmount) { @@ -137,7 +162,7 @@ export default class Request extends React.Component { this.setState({ fetching: true }); - return fetchDedupe(url, init, { requestKey, type }).then( + return fetchDedupe(url, init, { requestKey, contentType }).then( res => { responseCache[requestKey] = res; @@ -159,9 +184,8 @@ export default class Request extends React.Component { const globalObj = typeof self !== 'undefined' ? self : this; const AbortSignalCtr = globalObj.AbortSignal || function() {}; -Request.propTypes = { +Fetch.propTypes = { requestName: PropTypes.string, - children: PropTypes.func, fetchPolicy: PropTypes.oneOf([ 'cache-first', 'cache-and-network', @@ -169,7 +193,13 @@ Request.propTypes = { 'cache-only' ]), onResponse: PropTypes.func, - type: PropTypes.oneOf(['json', 'text', 'blob', 'arrayBuffer', 'formData']), + contentType: PropTypes.oneOf([ + 'json', + 'text', + 'blob', + 'arrayBuffer', + 'formData' + ]), transformResponse: PropTypes.func, lazy: PropTypes.bool, @@ -223,12 +253,11 @@ Request.propTypes = { signal: PropTypes.instanceOf(AbortSignalCtr) }; -Request.defaultProps = { - type: 'json', +Fetch.defaultProps = { + contentType: 'json', onResponse: () => {}, transformResponse: data => data, fetchPolicy: 'cache-first', - lazy: false, method: 'get', referrerPolicy: '', diff --git a/src/index.js b/src/index.js index bf4b653..a3fb7fb 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import Request from './request'; -import RequestComposer from './request-composer'; +import Fetch from './fetch'; +import FetchComposer from './fetch-composer'; -export { Request, RequestComposer }; +export { Fetch, FetchComposer }; diff --git a/test/index.test.js b/test/index.test.js index 9e5a867..d160ee4 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,5 +1,5 @@ -import { Request } from '../src'; +import { Fetch } from '../src'; test('Placeholder test', () => { - expect(Request).toBeTruthy(); + expect(Fetch).toBeTruthy(); });