Lightweight – no dependencies – React API for rest calls supporting Provider config and use hooks.
It's basically fetch
wrapped in a React Context providing React hooks and React Consumer.
So, this doc will not explain how all this concepts works but how this package wrap it.
Features:
- Configurable: Configure fetch calls, for custom headers or prefix url for example
- Flexible: Keep full fetch options to be overridable
- React compliant: Wrap fetch on React context
- Helper: Automatic resolve query params
- Safe: Don't mutate the
window.fetch
See a working demo on codesandbox : https://codesandbox.io/s/github/HollyPony/react-rest-api-samples
This package is currently served as-is. With usage of all ES6 features without any bundling and/or specific whatever the system is used.
- Proxiing config in order to define a config per method (or whatever the condition)
- Make unit tests great again
npm i react-rest-api
import React, { useState, useEffect, useReducer } from "react";
import ReactDOM from "react-dom";
import { ApiProvider, useApi } from "react-rest-api";
const defaultConfig = {
headers: {
// All api calls will take this Content-Type Header
"Content-Type": "application/json",
X_MY_APP_API_KEY: "XXX"
}
};
const App = () => {
// `url` and `config` refer respectively to `resource` and `init` from https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
const url = "https://jsonplaceholder.typicode.com";
const [config, setConfig] = useState(defaultConfig);
function signin(token) {
// Update the api config merging the current one
setConfig({
...defaultConfig,
headers: {
...defaultConfig.headers,
Authorization: token
}
});
}
// Consider a logout function
// function signout() {
// setConfig(defaultConfig);
// }
// All resolved response shoudl be converted to json according to the content type
function resolveHook(response) {
// Check the ok prop to consider the response as rejected as needed like:
return response.ok
? // `.json()` if ok
response.json()
: // if not `ok` fallback to rejectHook
Promise.reject(response);
}
// Treat fails here before returning to your call
// Note: The rejected reponse above will fall here
function rejectHook(response) {
return Promise.reject(response);
}
return (
<ApiProvider
url={url} // Optional: prefix url api calls. Litteraly, it's a prefix for api calls.
config={config} // Optional: Init default config of fetch. It can be overridable per calls later (or wrapping another ApiProvider)
resolveHook={resolveHook} // Optional: Provide callback function for success fetchs
rejectHook={rejectHook} // Optionnal: Provider reject callback
>
<SignIn signinCallback={signin} />
</ApiProvider>
);
};
const SignIn = ({ signinCallback }) => {
const api = useApi();
const [dataState, dataDispatch] = useReducer(reducer, {
status: "initialized",
payload: {}
});
function getSuccess() {
dataDispatch({ status: "initializing" });
// Call 'https://myendpoint.co/api/' + '/slug/details' + '?id=42&filter=random'
api
.get("/comments", undefined, { id: 42, filter: "random" })
.then((payload) => {
// Note that: payload is already jsonified due to resolveCallback
dataDispatch({ status: "initialized", payload });
})
.catch((payload) => {
// FeelsBadMan
dataDispatch({ status: "error", payload });
});
}
function postSuccess() {
dataDispatch({ status: "initializing" });
api
.post("/comments", {
body: JSON.stringify({ prop: "usefull stringify operation" })
})
.then((payload) => {
signinCallback("myToken");
dataDispatch({ status: "initialized", payload });
})
.catch((payload) => {
dataDispatch({ status: "error", payload });
});
}
function postError() {
dataDispatch({ status: "initializing" });
api
.post("/error", {
body: JSON.stringify({ prop: "usefull stringify operation" }),
headers: {
// This will override the default Content-type. Note the Authorization will be preserved
// :warning: This is for demo pruposes only, `body: JSON.stringify` should be treated as `application/json`. It's just to show a config override per call.
"Content-Type": "text/html; charset=utf-8"
}
})
.then((payload) => {
signinCallback("myToken");
dataDispatch({ status: "initialized", payload });
})
.catch((payload) => {
dataDispatch({ status: "error", payload });
});
}
return (
<>
<div>
<button onClick={getSuccess}>GET SUCCESS</button>
<button onClick={postSuccess}>POST SUCCESS</button>
<button onClick={postError}>POST ERROR</button>
</div>
{dataState.status === "initializing" && "loading ..."}
{dataState.status === "error" && (
<div>
Error:
<pre>{JSON.stringify(dataState.payload, null, 2)}</pre>
</div>
)}
{dataState.status === "initialized" && (
<div>
Success:
<pre>{JSON.stringify(dataState.payload, null, 2)}</pre>
</div>
)}
</>
);
};
// Wrap the reducer whatever we dont care that's not the point here
function reducer(state, action) {
return { ...state, ...action };
}
ReactDOM.render(<App />, document.getElementById("root"));
npm i react-rest-api
<ApiProvider
url={String}
config={Object}
resolveHook={Function}
rejectHook={Function}
/>
A string that prefix calls. Default to empty string.
Note: There is no extra managment for
/
. Keep in mind to add one at the end of url or add one before each api call,
An object gracefully merged for all calls prior to the caller. default to empty Object.
config
should be an object corresponding will send as fetch init
param by default for all api calls.
Overridable by config
param of all api methods.
Consider this methods as global fetch wrapper as:
fetch(...)
.then(resolveHook)
.catch(rejectHook)
because it's exactly how it's wrapped.
Note: fetch API consider a 400 response as success call with
ok: false
on then so. See the sample above for a solution. That's my main motivation to write this package.
import { useApi, apiContext } from 'react-rest-api'
Note that
useApi
is a basic wrapper foruseContext(apiContext)
Since it's a configured React context, you have a two native ways to access the apis + convienient hooks provided by this package.
This is the entry point to use the api.
There is no Consumer provided. Feel free to create your local own with the apiContext <apiContext.Consumer />
lessgo.
Props Available through the useApi
/ useContext(apiContext)
: const api = useApi()
api.fetch(url: string, config: Object, queryParams: Object) : Promise
This function is the most important part the main motivation of this project.
-
url
: the called url. If one is define in ApiProvider, it will be concatenate. There is currently no transformation arounds the slashes/
treatment so don't miss yours ;) -
config
: merge the object with config of ApiProvider. -
queryParams
: is an Object for query params, under the hood useURLSearchParams
to build a string then concat tourl
with a?
.api.get('localhost', undefined, {param1: 'value1'})
result to a call onlocalhost?param1=value1
queryParams take care of list.
{ type: [1, 2, 3] }
result to?type=1&type=2&type=3
Dates object will be parsed with
toISOString
method. If you wont this behaviour, parse the date before the call, ie. don't pass Date Object.
api.get(url: string, config: Object, queryParams: Object) : Promise
api.post(url: string, config: Object, queryParams: Object) : Promise
api.put(url: string, config: Object, queryParams: Object) : Promise
api.del(url: string, config: Object, queryParams: Object) : Promise
This methods are wrapper for api.fetch
with pre-defined config: {method: 'METHOD'}
respectively GET
, POST
, PUT
and DELETE
Note: You can call
api.get('localhost', method: 'POST'})
. This will result to an effective POST call, overriding the method. config on call is always the best.