With useApi
hook, we standardize API calls and response handling in our React based projects at TechNarts. It is a simple wrapper around the fetch API with some useful states.
useApiReporter
hook is a complementary tool for useApi
. It is used to make callbacks on request start & end and handling errors.
$ npm i @technarts/react-use-api
import { useApi } from "@technarts/react-use-api";
// Get an ApiCounsel by which you can make calls and manage responses:
const apiGetter = useApi<{ status: string, data: number[] }>({ url: "your/url/here", method: "GET" });
React.useEffect(() => {
apiGetter.call().then(({ resp, err, fault }) => {
if (resp) {
// Do what is needed with the response.
}
});
}, [])
// Some commonly used headers are explicitly defined:
type Headers = {
"Accept"?: string,
"Authorization"?: string,
"Content-Type"?: string,
[k: string]: any
}
// Parameters accepted by useApi:
type Params = {
url: string,
method: "POST" | "PUT" | "PATCH" | "DELETE" | "GET" | "DOWNLOAD" | "UPLOAD"
responseGuard?: (r: Response, params: CallParams) => Promise<Response>,
headers?: Headers,
}
// Parameters accepted by the call method of ApiCounsel.
// Maybe omitted altogether if nothing needs to be overriden:
type CallParams = {
url?: string,
responseGuard?: (r: Response, params: CallParams) => Promise<Response>,
headers?: Headers,
payload?: any,
}
// Effective return type of the call method of ApiCounsel:
export type CallResult<T> = {
resp: T | null,
error: any,
fault: any,
}
// The return type of useApi:
type ApiCounsel<T> = {
RESP: T | null, // Response will be available here when the request successfully completes.
inFlight: boolean, // Whether the request is pending or not.
error: any, // HTTP Response status was anything outside of [200, 299], such as HTTP 404.
fault: any, // Server didn't respond.
call: (callParams?: CallParams) => Promise<CallResult<T>>, // Request trigger.
url: string, // The same url as what is passed to useApi inside params. Handy when apiCounsel is passed to another React component where the url is no longer in the scope.
}
The generic type T passed to useApi
(like the following):
const apiPoster = useApi<{ status: "ok" | "nok", data: number[] }>({
url: "...",
method: "POST"
})
// Here type T = { status: "ok" | "nok", data: number[] }
will end up RESP
being the very type of T
:
React.useEffect(() => {
if (apiPoster.RESP && apiPoster.RESP.status === "ok") {
// Do whatever needed with apiPoster.RESP.data
}
}, [apiCounsel.RESP])
Response guard welcomes the response. So if every 401 needs to be handled the same way, it can be used as follows:
function guard(r: Response, params: CallParams): Promise<Response> {
if (r.status === 401) {
// Handle 401, maybe locate to login?
}
return Promise.resolve(r);
}
// T is set to `any` for keeping examples simple:
const apiGetter = useApi<any>({ url: "...", method: "GET", responseGuard: guard })
const apiPoster = useApi<any>({ url: "...", method: "POST", responseGuard: guard })
If it is way too prevalent in the project, useApi itself can be wrapped so that every apiCounsel has its response guard:
function _useApi<T>(params: Omit<Params, "responseGuard">): ApiCounsel<T> {
return useApi<T>({ ...params, responseGuard: guard });
}
Sometimes it is necessary to alter what is passed to useApi
while making API calls. One simple example is setting the url according to the request:
const apiServiceGetter = useApi<any>({ url: "/services", method: "GET" })
React.useEffect(() => {
apiServiceGetter.call({ url: `${apiServiceGetter.url}?id=1` })
}, [])
Handling the response directly within the context where the API call is made:
const apiGetter = useApi<any>({ url: "...", method: "GET" })
React.useEffect(() => {
apiGetter.call().then((result) => {
const { resp, err, fault } = result // result is of type CallResult.
// Rest of the code...
})
}, [])
Result can be handled outside of the context where the API call is made, e.g. in a useEffect
.
RESP
, error
and fault
in type ApiCounsel
are mutually exclusive:
const apiCounsel = useApi<any>({ url: "...", method: "GET" });
// Make the call
React.useEffect(() => {
apiCounsel.call();
}, [])
// Listen for the result
React.useEffect(() => {
if (apiCounsel.fault)
// No response at all.
else if (apiCounsel.error)
// Anything other than 200-299.
else
// apiCounsel.RESP is here.
}, [apiCounsel.RESP, apiCounsel.error, apiCounsel.fault])
Please check useApiReporter
for an alternative way of handling the result.
inFlight
property of ApiCounsel
can be observed for the requests's pending status:
React.useEffect(() => {
if (!apiCounsel.inFlight) {
// It is safe to issue another call...
}
}, [apiCounsel.inFlight])
Also, useApiReporter
's end
callback can be used for chaining requests one after another.
This hook can be used for making callbacks when a request started and/or ended. Convenient when an ApiCounsel is called in numerous places but the necessary callbacks are the same.
// type T = { ... }
const apiGetter = useApi<T>({ url: "...", method: "GET" })
useApiReporter({
apiCounsel: apiGetter,
start: () => console.log("Request started"),
end: (resp, error, fault) => {
if (fault) {
// No response at all.
} else if (error) {
// Anything other than 200-299.
} else {
// resp is of type T and is not null.
}
console.log("Request ended")
}
})
Inspired by: https://betterprogramming.pub/clean-api-call-with-react-hooks-3bd6438a375a (Author: Ashraful Islam, thanks)
What is good about this approach is, API calls become fairly consistent and being able to handle the start and the end of requests in a standardized way gives a fine grained control over handling loadings.
On the flip side, one apiCounsel
is meant to be used only for one kind of method of the API end point. For example, if you have apiServiceGetter
which you defined for listing all services and probably also for retrieving details of a single service, you can't use it for updating a service. Is this a problem? Not, really.
Consider that it were possible to make arbitrary GET
and POST
calls with one apiCounsel
. So it would be like:
// Purely hypothetical code snippet:
const serviceApi = useApi<any>("bla/bla/services")
React.useEffect(() => {
serviceApi.get();
}, [/* bla bla */])
React.useEffect(() => {
serviceApi.post(payload);
}, [/* bla bla */])
Looks neat. resp
, error
and fault
states need to be handled inside useApi
separately for every HTTP method out there. useApiReporter
needs to consider HTTP method type too. End points lacking some of the methods should not cause trouble.
In fact, we tried this approach before. Admission: it was working (with a few flaws here and there). But we ended up making apiCounsel
less responsible (a single call() to make the request) for the sake of simplicity.
Since one apiCounsel
is responsible for one type of API call, naming apiCounsel
s in an informative way helps:
const apiServiceGetter = useApi<any>({ url: "...", method: "GET" })
const apiServicePatcher = useApi<any>({ url: "...", method: "PATCH" })
So the reader knows when apiServicePatcher.call()
is issued, it will send a PATCH
request. But, if there is only one apiCounsel
in a component, for example a ComboBox needs only one for fetching the records matching the search keywords from the url provided to it, simply naming the apiCounsel
as apiGetter
does the job.