react-http-fetch
Version:
An http library for React JS built on top of native JS fetch
779 lines (637 loc) • 23.9 kB
Markdown
<h1 align="center">React http fetch</h1>
<p align="center">
<img src="https://github.com/nebarf/react-http-fetch/blob/main/assets/img/react-http-fetch-logo.png?raw=true" alt="react-http-fetch logo"/>
<br>
<i>A http library for React JS built on top of JS native fetch.</i>
<br>
</p>
<p align="center">
<a href="CONTRIBUTING.md">Contributing Guidelines</a>
·
<a href="CHANGELOG.md">Changelog</a>
<br>
<br>
</p>
<p align="center">
<a href="https://github.com/nebarf/react-http-fetch/actions/workflows/status-check.yml">
<img src="https://github.com/nebarf/react-http-fetch/actions/workflows/status-check.yml/badge.svg" alt="Build status" />
</a>
<a href="https://www.npmjs.com/react-http-fetch">
<img src="https://img.shields.io/npm/v/react-http-fetch.svg?logo=npm&logoColor=fff&label=NPM+package&color=limegreen" alt="react-http-fetch on npm" />
</a>
<a href="http://opensource.org/licenses/MIT">
<img src="https://img.shields.io/npm/l/react-http-fetch.svg?color=lime-green" alt="MIT license" />
</a>
</p>
<hr>
<br>
Just follow links below to get an overview of library features.
- [Contents](
- [Getting started](
- [Provider](
- [Http client](
- [Public API](
- [Request params](
- [Request return](
- [Abortable request return](
- [Example – Abortable request](
- [Example – Get request](
- [Example – Http context](
- [Request hooks](
- [Http request hook params](
- [Http request hook return](
- [Http request state](
- [Example – Http request hook triggered automatically on component mount](
- [Example – Http request hook triggered manually on component mount](
- [Example – Non-abortable http request hook](
- [Example – Aborting http request triggered by the hook](
- [Example – Http post request hook](
- [Events](
- [Caching](
- [Browser support](
<br>
Install the package by using npm
```
npm install react-http-fetch
```
or yarn
```
yarn add react-http-fetch
```
You can override the default configuration used by the http client to perform any request by using the `HttpClientConfigProvider`:
```js
import React from 'react';
import { defaultHttpReqConfig, HttpClientConfigProvider } from 'react-http-fetch';
function Child() {
return (
<div> Child component </div>
);
};
function httpResponseParser(res) {
return res.json();
}
function App() {
/**
* Provided configs are automatically merged to the default one.
*/
const httpReqConfig = {
// ...defaultHttpReqConfig,
baseUrl: process.env.BACKEND_URL,
responseParser: httpResponseParser,
reqOptions: {
headers: {
'Content-Type': 'text/html; charset=UTF-8',
},
},
};
return (
<HttpClientConfigProvider config={httpReqConfig}>
<Child />
</HttpClientConfigProvider>
);
}
export default App;
```
Below the complete set of options you can provide to the `HttpClientConfigProvider`:
| Option | Description | Default |
| --------------------- | --------------------------------------------------------------------------|------------- |
|baseUrl|The base url used by the client to perform any http request (e.g. http://localhost:8000)|```''```
|responseParser|A function that maps the native fetch response. The default parser transform the fetch response stream into a json (https://developer.mozilla.org/en-US/docs/Web/API/Response/json)|[httpResponseParser](src/config/response-parser.ts)
|requestBodySerializer|A function used to serialize request body. The default serializer take into account a wide range of data types to figure out which type of serialization to perform|[serializeRequestBody](src/config/request-body-serializer.ts)
|reqOptions|The default request option that will be carried by any request dispatched by the client. See [HttpRequestOptions](src/client/types.ts)|```{ headers: { 'Content-Type': 'application/json' } }```
|cacheStore|The store for cached http responses. By default an in-memory cache store is used.|[HttpInMemoryCacheStore](src/cache/http-in-memory-cache-store.ts)
|cacheStorePrefix|The prefix concatenated to any cached entry.|`rfh`
|cacheStoreSeparator|Separates the store prefix and the cached entry identifier|`__`
<br>
The `useHttpClient` hook return a set of methods to perform http requests. The `request` function is the lowest level one, all other exposed functions are just decorators around it. Below a basic example using `request`:
```js
import React from 'react';
import { useHttpClient } from 'react-http-fetch';
function App() {
const { request } = useHttpClient();
const [todo, setTodo] = useState();
useEffect(
() => {
const fetchTodo = async () => {
const res = await request({
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
relativeUrl: 'todos/1',
requestOptions: {
method: 'GET',
},
});
setTodo(res);
};
fetchTodo();
},
[]
);
return (
<div>{`Todo name: ${todo && todo.title}`}</div>
);
}
export default App;
```
The complete *public API* exposed by the hook:
| Method | Description | Params | Return |
| --------------------- | --------------------------------------------------------------------------| --------------------- | --------------------- |
|request | The lowest level method to perform a http request | [Request params](
| <ul class="httpRequestsList"><li>get</li><li>post</li><li>put</li><li>patch</li><li>delete</li></ul> | Make use of lower level method `request` by just overriding the http method ([example](#example--abortable-request)) | [Request params](#request-params) | [Request return](#request-return)
| abortableRequest | The lowest level method to perform an abortable http request ([example](#example--abortable-request)) | [Request params](#request-params) | [Abortable request return](#abortable-request-return)
| <ul class="httpRequestsList"><li>abortableGet</li><li>abortablePost</li><li>abortablePut</li><li>abortablePatch</li><li>abortableDelete</li></ul> | Make use of lower level method `abortableRequest` by just overriding the http method | [Request params](
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| baseUrlOverride | string | The base url of the request. If provided, it would override the [provider](
| relativeUrl | string | The url relative to the base one (e.g. posts/1).
| parser | [HttpResponseParser](src/client/types.ts) | An optional response parser that would override the [provider](#provider) global one. |
| context | [HttpContext](src/client/http-context.ts) | An optional context that carries arbitrary user defined data. See examples.|
| requestOptions | [HttpRequestOptions](./src/client/types.ts) | The options carried by the fetch request. |
### Request return
The jsonified return value of native JS fetch. If a custom response parser (see [Provider](#provider)) is provided then the return value corresponds to the parsed one.
### Abortable request return
| Value | Type |
| ----- | ---- |
|[request, abortController]|[[RequestReturn](#request-return), [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)]|
### Example – Abortable request
```js
import React, { useState, useRef } from 'react';
import { useHttpClient } from 'react-http-fetch';
function App() {
const { abortableRequest } = useHttpClient();
const abortCtrlRef = useRef();
const [todo, setTodo] = useState();
const fetchTodo = async () => {
const [reqPromise, abortController] = abortableRequest({
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
relativeUrl: 'todos/1',
});
abortCtrlRef.current = abortController;
try {
const res = await reqPromise;
setTodo(res);
} catch (error) {
// Abort the request will cause the request promise to be rejected with the following error:
// "DOMException: The user aborted a request."
console.error(error);
} finally {
abortCtrlRef.current = undefined;
}
};
const abortPendingRequest = () => {
if (abortCtrlRef.current) {
abortCtrlRef.current.abort();
}
};
return (
<div style={{ margin: '20px' }}>
<div>{`Todo name: ${todo && todo.title}`}</div>
<button
style={{ marginRight: '10px' }}
type="button"
onClick={fetchTodo}
>
Do request
</button>
<button
type="button"
onClick={abortPendingRequest}
>
Abort
</button>
</div>
);
}
export default App;
```
```js
import React, { useState, useEffect } from 'react';
import { useHttpClient } from 'react-http-fetch';
function App() {
const { get } = useHttpClient();
const [todo, setTodo] = useState();
useEffect(
() => {
const fetchTodo = async () => {
const res = await get({
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
relativeUrl: 'todos/1',
});
setTodo(res);
};
fetchTodo();
},
[]
);
return (
<div>{`Todo name: ${todo && todo.title}`}</div>
);
}
export default App;
```
```js
import React, { useEffect } from 'react';
import {
useHttpClient,
useHttpEvent,
RequestStartedEvent,
HttpContextToken,
HttpContext, } from 'react-http-fetch';
const showGlobalLoader = new HttpContextToken(true);
const reqContext = new HttpContext().set(showGlobalLoader, false);
function App() {
const { request } = useHttpClient();
useHttpEvent(RequestStartedEvent, (payload) => {
console.log('Show global loader:', payload.context.get(showGlobalLoader));
});
useEffect(
() => {
const fetchTodo = async () => {
await request({
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
relativeUrl: 'todos/1',
context: reqContext,
});
};
fetchTodo();
},
[]
);
return (
<h1>Http Context</h1>
);
}
export default App;
```
<br>
The library provides a hook `useHttpRequest` managing the state of the http request. Such state is returned by the hook along with a function to trigger the request. See [params](
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| baseUrlOverride | string | The base url of the request. If provided, it would override the [provider](
| relativeUrl | string | The url relative to the base one (e.g. posts/1).
| parser | [HttpResponseParser](src/client/types.ts) | An optional response parser that would override the [provider](#provider) global one. |
| context | [HttpContext](src/client/http-context.ts) | An optional context that carries arbitrary user defined data. See examples.|
| requestOptions | [HttpRequestOptions](./src/client/types.ts) | The options carried by the fetch request. |
| initialData | any | The value that the state assumes initially before the request is send. |
| fetchOnBootstrap | boolean | Tell if the fetch must be triggered automatically when mounting the component or not. In the second case we would like to have a manual fetch, this is optained by a request function returned by the hook. |
### Http request hook return
Returns an array of three elements:
- The first one embeds the state of the http request.
- The second is a function that can be used to perform an abortable http request.
- The third is a function that can be used to perform a non-abortable http request.
See examples for further details. The table below describes the shape (i.e. properties) of http request state.
### Http request state
| Property | Type | Description |
| --------- | ---- | ----------- |
| pristine | boolean | Tells if the request has been dispatched. |
| errored | boolean | Tells if the request has returned an error. |
| isLoading | boolean | Tells if the request is pending. |
| error | unknown | property evaluated by the error generated by the backend api. |
| data | any | The response provided by the backend api. |
### Example – Http request hook triggered automatically on component mount
```js
import React from 'react';
import { useHttpRequest } from 'react-http-fetch';
function App() {
const [state] = useHttpRequest({
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
relativeUrl: 'todos/1',
requestOptions: {},
initialData: {},
fetchOnBootstrap: true,
});
return (
<div>{`Todo name: ${(state.data && state.data.title) || 'unknown'}`}</div>
);
}
export default App;
```
<br>
```js
import { useHttpRequest } from 'react-http-fetch';
import React, { useEffect } from 'react';
function App() {
const [state, request] = useHttpRequest({
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
relativeUrl: 'todos/1',
});
useEffect(() => {
const { reqResult, abortController } = request();
reqResult
.then(res => console.log('request response', res))
.catch(err => console.error(err));
// You can use the returned AbortController instance to abort the request
// abortController.abort();
}, [request]);
return (
<div>{`Todo name: ${(state.data && state.data.title) || 'unknown'}`}</div>
);
}
export default App;
```
```js
Placeholder
import React, { useEffect } from 'react';
import { useHttpRequest } from 'react-http-fetch';
function App() {
const [state, , request] = useHttpRequest({
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
relativeUrl: 'todos/1',
});
useEffect(() => request(), [request]);
return (
<div>{`Todo name: ${(state.data && state.data.title) || 'unknown'}`}</div>
);
}
export default App;
```
```js
import { useHttpRequest } from 'react-http-fetch';
import React, { useRef } from 'react';
function App() {
const [state, request] = useHttpRequest({
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
relativeUrl: 'todos/1',
});
const abortCtrlRef = useRef();
const fetchTodo = () => {
abortPendingRequest();
const { reqResult, abortController } = request();
abortCtrlRef.current = abortController;
reqResult
// Abort the request will cause the request promise to be rejected with the following error:
// "DOMException: The user aborted a request."
.catch(err => console.error(err));
};
const abortPendingRequest = () => {
if (abortCtrlRef.current) {
abortCtrlRef.current.abort();
}
};
return (
<div style={{ margin: '20px' }}>
<div>{`Todo name: ${(state.data && state.data.title) || 'unknown'}`}</div>
<button
style={{ marginRight: '10px' }}
type="button"
onClick={fetchTodo}
>
Do request
</button>
<button
type="button"
onClick={abortPendingRequest}
>
Abort
</button>
</div>
);
}
export default App;
```
```js
import React, { useState } from 'react';
import { useHttpPost } from 'react-http-fetch';
function App() {
const [inputs, setInputs] = useState({});
const handleChange = (event) => {
const name = event.target.name;
const value = event.target.value;
setInputs(values => ({...values, [name]: value}))
}
const [, createPostRequest] = useHttpPost({
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
relativeUrl: 'posts',
});
const createPost = async (event) => {
event.preventDefault();
const { postTitle, postBody } = inputs;
const reqBody = { title: postTitle, body: postBody };
try {
// Providing request options when running the request.
// Provided options will be merged to the one provided
// to the hook useHttpPost.
await createPostRequest({
requestOptions: { body: reqBody }
});
alert('Post created!');
} catch (error) {
console.error(error);
alert('An error occured. Check the browser console.');
}
};
return (
<form onSubmit={createPost}>
<label style={{ display: 'block' }}>
Title:
<input
type="text"
name="postTitle"
value={inputs.postTitle || ""}
onChange={handleChange}
/>
</label>
<label style={{ display: 'block' }}>
Body:
<input
type="text"
name="postBody"
value={inputs.postBody || ""}
onChange={handleChange}
/>
</label>
<button type="submit">
Create Post
</button>
</form>
);
}
export default App;
```
<br>
Every time a request is executed the events shown below will be emitted. Each event carries a specific payload.
| Event type | Payload type |
| --------- | ---- |
| [RequestStartedEvent](src/events-manager/events/request-started-event.ts) | [HttpRequest](src/client/http-request.ts) |
| [RequestErroredEvent](src/events-manager/events/request-errored-event.ts) | [HttpError](src/errors/http-error.ts) |
| [RequestSuccededEvent](src/events-manager/events/request-succeded-event.ts) |[RequestSuccededEventPayload](src/events-manager/events/request-succeded-event.ts) |
You can subscribe a specific event using the [useHttpEvent](src/events-manager/use-http-event.ts) hook as shown below:
```js
import { useState } from 'react';
import { RequestErroredEvent, RequestStartedEvent, RequestSuccededEvent, useHttpEvent, useHttpRequest } from 'react-http-fetch';
function App() {
const [count, setCount] = useState(0);
const [, request] = useHttpRequest({
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
relativeUrl: 'todos/1',
});
useHttpEvent(RequestStartedEvent, () => setCount(count + 1));
useHttpEvent(RequestSuccededEvent, () => setCount(count > 0 ? count - 1 : 0));
useHttpEvent(RequestErroredEvent, () => setCount(count > 0 ? count - 1 : 0));
return (
<>
<button onClick={request}>{'increment count:'}</button>
<span>{count}</span>
</>
);
}
export default App;
```
<br>
Any request can be cached by setting the `maxAge` (expressed in milliseconds) parameter as part of the request options as shown below:
```js
import { useHttpRequest } from 'react-http-fetch';
import React from 'react';
function App() {
const [state, request] = useHttpRequest({
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
relativeUrl: 'todos/1',
requestOptions: { maxAge: 60000 } // Cache for 1 minute
});
const fetchTodo = () => {
const { reqResult } = request();
reqResult.then(res => console.log(res))
};
return (
<>
<div>
{`Todo name: ${(state && state.data && state.data.title) || ''}`}
</div>
<button type="button" onClick={fetchTodo}>
Make request
</button>
</>
);
}
export default App;
```
By default the http client uses an in-memory cache, so it will be flushed everytime a full app refresh is performed. You can override the default caching strategy by providing your own cache store. The example below shows a http cache store based on session storage:
```js
import React from 'react';
import { useHttpRequest, HttpClientConfigProvider } from 'react-http-fetch';
export class HttpSessionStorageCacheStore {
/**
* The local cache providing for a request identifier
* the corresponding cached entry.
*/
_store = window.sessionStorage;
/**
* @inheritdoc
*/
get(identifier) {
const stringifiedEntry = this._store.getItem(identifier);
if (!stringifiedEntry) {
return;
}
try {
const parsedEntry = JSON.parse(stringifiedEntry);
return parsedEntry;
} catch (err) {
return;
}
}
/**
* @inheritdoc
*/
put(identifier, entry) {
try {
const stringifiedEntry = JSON.stringify(entry);
this._store.setItem(identifier, stringifiedEntry);
return () => this.delete(identifier);
} catch (err) {
return () => {};
}
}
/**
* @inheritdoc
*/
has(identifier) {
return this._store.has(identifier);
}
/**
* @inheritdoc
*/
delete(identifier) {
this._store.removeItem(identifier);
}
/**
* Gets all entry keys.
*/
_keys() {
return Object.keys(this._store);
}
/**
* Gets all stored entries.
*/
entries() {
return this._keys()
.map(entryKey => this._store.getItem(entryKey));
}
/**
* @inheritdoc
*/
flush() {
this._keys().forEach((itemKey) => {
this.delete(itemKey);
});
}
}
const httpCacheStore = new HttpSessionStorageCacheStore();
function Child() {
const [state, request] = useHttpRequest({
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
relativeUrl: 'todos/1',
requestOptions: { maxAge: 60000 } // Cache for 1 minute
});
console.log('Request state:', state.data);
const fetchTodo = () => {
const { reqResult } = request();
reqResult.then(res => console.log('Request response: ', res))
};
return (
<>
<div>
{`Todo name: ${(state && state.data && state.data.title) || ''}`}
</div>
<button type="button" onClick={fetchTodo}>
Make request
</button>
</>
);
};
function App() {
const httpReqConfig = {
cacheStore: httpCacheStore,
// "prefix" and "separator" are not mandatory,
// if not provided the default ones will be used.
cacheStorePrefix: 'customPrefix',
cacheStoreSeparator: '-'
};
return (
<HttpClientConfigProvider config={httpReqConfig}>
<Child />
</HttpClientConfigProvider>
);
}
export default App;
```
<br>
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://gotbahn.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://gotbahn.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://gotbahn.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://gotbahn.github.io/browsers-support-badges/)</br>Safari |
| --------- | --------- | --------- | --------- |
| last 2 versions| last 2 versions| last 2 versions| last 2 versions