@cutting/react-abortable-fetch v0.36.4
@cutting/react-abortable-fetch
Quick Start
Here is the simplest form of using react-abortable-fetch:
# npm
npm install @cutting/react-abortable-fetch
# yarn
yarn add @cutting/react-abortable-fetchimport { useFetch } from '@cutting/react-abortable-fetch';
const { data, error, state } = useFetch(`/api/users/1`);
if (state === 'loading') {
return <div>loading...</div>
}
if (error) {
return <div>Houston, we have a problem</dov>
}
return <SomeComponent data={data}/>Here is a codesandbox with a more complex example.
codesandboxes
- Simple example
- Simple example with click handler
- multi-query and abortable example
- multi-query and accumulation example
Table of contents
- In the Name of Oden, Why?
- Examples
- Accumulation
- Nested Queries
- Retries
- Timeout
- API
- Typescript
- codesandboxes
- TODO
Why
For crying out loud, why have you created yet another
react-queryoruse-queryoruse-fetchclone? Are you serious?
Yes, because I am frustrated with what is out there.
Packages like react-query are excellent, but they are becoming quite bloated and complicated when all I want to do is to call an endpoint. I need to spend time learning how to configure a custom React context and a whole bunch of other stuff when all I want to do is give a function a URL and, I am good to go.
Here are some other annoyances:
I want to be productive ASAP. I don't want to have to read a lot of documentation or have to join a discord channel to learn how to use the package.
Give me intelligent defaults. I think there is still way too much setup and config in any
useQueryXXXoruseFetchXXXthingy that I have tried. JavaScript/Typescript has descended into a sea of endless configuration which means I have to understand everything intimately to use it.Packages like react-query are excellent, but I still have to provide the code to use
axiosorfetchor whatever, surely the whole point of using a package like this is to abstract all that stuff away. This package uses cross-fetch and you cannot change this. I am predicting that you do not need to.I want
abortto be a first-class citizen and not left up to me to get my hands dirty withAbortControllers.I want an adjustable timeout, queries should not be allowed to run forever. I don't want to have to roll my own logic.
I want to run more than one query.
I don't want to deal with cache keys. The framework should handle this.
I really, really, really do NOT want to do stuff like this:
useOverlyComplicatedFetch(['/api/users', id], (url, id) => query(url, { id }))I don't want to configure a react context to execute a single query. The context should be optional.
I really, really, really do not want a set of conflicting and contradictory boolean flags like
isLoading,isErroretc. I want a mutually exclusivestatestring union that can only ever be one value?type State = 'ready' | 'loading' | 'succeeded' | 'error' | 'aborted';I needed json and jsonp
I could go on, but I will leave it there for now....
Examples
Simple but nice
Give me a URL and the framework will do the rest. I am personally weary of endless configuration when all I want to do is this:
const { data, error } = useFetch(`/api/users/1`);
if (typeof data !== 'undefined') {
console.log(data);
}or if you want to invoke the query in a button click handler, then you can do this:
const { run, state } = useFetch(`/api/users/1`, { executeOnMount: false });
// or use a combination of Request and RequestInfo
// const { run, state } = useFetch({url: `/api/users/1`, method: 'POST'}, { executeOnMount: false });
return (
<button
disabled={state !== 'ready'}
onClick={() => {
run({ an: 'object'}); // by default objects will be seriaized to json
}}
>
DO IT
</button>
);or
Multi Queries
I wrote this package because I could not find anything that did multi-queries and had first class abort functionality.
There are a couple of ways of executing multi queries.
Just load up the URLs into an array and optionally use some of the handlers:
const { run, data, state, abort, reset } = useFetch<Result[], Product>(
[
"https://reqres.in/api/products/1?delay=1",
"https://reqres.in/api/products/2?delay=1",
"https://reqres.in/api/products/3?delay=1",
"https://reqres.in/api/products/4?delay=1",
"https://reqres.in/api/products/5?delay=1",
"https://reqres.in/api/products/6?delay=1",
"https://reqres.in/api/products/7?delay=1",
"https://reqres.in/api/products/8?delay=1",
"https://reqres.in/api/products/9?delay=1",
"https://reqres.in/api/products/10?delay=1"
],
// or use a combination of Request and RequestInfo
// const { run, data, state, abort, reset } = useFetch<Result[], Product>(
// [
// {url: "https://reqres.in/api/products/1?delay=1", method: 'POST'}
// {url: "https://reqres.in/api/products/1?delay=2", method: 'POST'}
// etc.
{
initialState: [],
executeOnMount: false,
accumulator(acc, current) {
acc.push({
id: current.data.id,
name: current.data.name,
year: current.data.year
});
return acc;
},
onQuerySuccess(product) {
assert(!!product, `no product in onQuerySuccess`);
setProgress((n) => (n += 1));
setMessages((m) => {
m.push(`received product ${product.data.name}`);
return m;
});
},
onSuccess: (result) => {
console.log(result);
console.log(`Downloaded ${result?.length} products`);
},
onAbort: () => {
setMessages(["We have aborted"]);
},
onError: (e) => {
console.log("in global error handler");
console.error(e.message);
}
}
);
---
// Now we really need abort
<button onClick={() => abort()}>
CANCEL
</button>Builder Syntax
Alternatively, useFetch can take a builder function that provides a fetchClient object with an addFetchRequest function.
const { state, abort, data } = useFetch(
(fetchClient) => {
for (const i of [...Array.from({ length: 10 }).keys()]) {
fetchClient.addFetchRequest(`/api/users/${i + 1}`, {
onQuerySuccess: (d) => console.log('you can add a different handler for each query'),
onQueryError(e) {
console.log(`scoped error handler`);
console.error(e);
},
});
}
return fetchClient;
},
{
onQuerySuccess: (d) => console.log('optional handler that gets called when a query has completed successfully'),
initialState: [],
onSuccess: () => {
console.log(`optional onSuccess handler`);
},
onAbort: () => {
console.log('optional onAbort' )
},
onError: (e) => {
console.log('optional onError handler');
console.error(e.message);
},
},
);Accumulation
When executing multiple queries, think of useFetch like a reduce or a fold function.
Default accumulation
If the initialState field is an array, then useFetch will append the result of each async fetch onto the initialState.
const { data } = useFetch(
[ '/add/1/1', '/add/2/2', '/add/3/3' ],
{
initialState: []
},
);
if (typeof data !== 'undefined') {
console.log(data); // [2, 4, 6];
}Custom accumlator function
You can supply an accumulator function that executes each time a remote query resolves. You can build up the final result in this accumulator function.
const { data } = useFetch(
[ '/add/1/1', '/add/2/2', '/add/3/3' ],
{
initialState: 0,
accumulator: (acc, current) => acc + current.answer,
},
);
if( typeof data !== 'undefined') {
console.log(`the grand total is ${data}`), // the grand total is 12
}WARNING! The operations should be commutative because there is no guarantee when the async functions will return.
Nested-Queries
A common scenario is to run one or more async request with the results of the first request. The Accumulation function can also run async and an AccumulationContext is supplied to the accumulator function as the 3rd argument.
useFetch<Vendor[], typeof vendors>('http://localhost:3000/vendors', {
executeOnMount: false,
onSuccess,
onError,
onAbort,
initialState: [],
accumulator: async (acc, v, { fetcher }) => {
for (const vendor of v.data) {
const request = await fetcher(`http://localhost:3000/vendors/${vendor.id}/items`);
const items = await request.json();
acc.push({
...vendor,
...items,
});
}
return acc;
},
}),The AccumulationContext has a fetcher field that is the fetch object that you can run queries against.
The AccumulationContext has the following fields:
export interface AccumulationContext {
request: RequestInfo;
fetcher: typeof nativeFetch | typeof fetchJsonp;
}Retries
By default react-abortable-fetch will retry any request that returns a non 2xx range response 3 times with a 500ms delay.
The retryAttempts and retryDelay properties can configure this differently:
const { data, error } = useFetch(`/flaky-connection`, {
retryAttempts: 5,
retryDelay: 1000,
});Timeout
By default each fetch request has a generous default timeout property of 180000ms to complete before timing out.
API
useFetch(string url or array of string urls, or builder, options)
const { state, abort, reset, run } = useFetch(
[ '/add/1/1', '/add/2/2', '/add/3/3' ],
{
initialState: 0,
accumulator: (acc, current) => acc + current.answer,
retryAttempts: 5,
retryDelay: 100,
timeout: 500000,
method: 'POST',
fetchTye: 'jsonp',
executeOnMount: false,
onQuerySuccess: (d) => console.log('optional handler that gets called when a single query has completed successfully'),
onSuccess: () => {
console.log(`optional overall onSuccess handler`);
},
onAbort: () => {
console.log('optional onAbort' )
},
onError: (e) => {
console.log('optional onError handler');
console.error(e.message);
},
},
);| Prop | Description | Default |
|---|---|---|
| initialState | The initial value. Can be important for accumulation and multiple queries. THe initial state can be built up or accumulated as each query resolves. | undefined |
| accumulator | function that can be used to transform the result of every query. | default-accumulator.ts |
| method | http verb, GET, POST, PUT or DELETE etc. | GET |
| executeOnMount | whether or not the remote fetch executes after the DOM paint event | a controversial true |
| fetchType | which fetch to call. The default fetch will use cross-fetch or fetch-jsonp. | fetch |
| retryAttempts | how many times a fetch operation will be retried | 3 |
| retryDelay | the time in ms between each retry in the event of a fail | 500 |
| timeout | The length of time each request is allowed to run without a response before aborting | 5000ms |
| onSuccess | callback that is called with the either the result of a single value or if running multiple queries then the accumulated result | no op |
| onError | callback that is called with the error in the event of a failed query | |
| onQuerySuccess | everytime a query resolves, this function is called with the result of the fetch query. | no op |
| onQueryError | handler for individual query errors | no op |
Typescript
The useFetch function signature looks like this:
export function useFetch<R, T = undefined>(url: string, options?: UseFetchOptions<R, T>): QueryResult<R>;
export function useFetch<R, T = undefined>(urls: string[], options?: UseFetchOptions<R, T>): QueryResult<R>;
export function useFetch<R, T = undefined>(
fetchRequestInfo: FetchRequestInfo,
options?: UseFetchOptions<R, T>,
): QueryResult<R>;
export function useFetch<R, T = undefined>(
fetchRequestInfo: FetchRequestInfo[],
options?: UseFetchOptions<R, T>,
): QueryResult<R>;
export function useFetch<R, T = undefined>(builder: Builder<R, T>, options?: UseFetchOptions<R, T>): QueryResult<R>;
export function useFetch<R, T = undefined>(
builderOrRequestInfos: string | string[] | FetchRequestInfo | FetchRequestInfo[] | Builder<R, T>,
options: UseFetchOptions<R, T> = {},
): QueryResult<R> {Ais the type for the data that is returned from a fetcg request.Ris the type for the end result of auseFetchoperation.Rwill default toAif it is not explicitly set. Consider the example below where the data returned from of an individual fetch is{ answer: number }but the end result of applying the accumlator function to all the requests is anumbertype.const { data } = useFetch<{ answer: number }, number>( ['http://localhost:3000/add/1/1', 'http://localhost:3000/add/2/2', 'http://localhost:3000/add/3/3'], { initialState: 0, accumulator: (acc, current) => acc + current.answer, // current is type { answer: number } } if ( data ) { console.log(typeof data); // number }
Type inference
useFetch will infer the A type from the initialState field if this property has been set
const { data } = useFetch(
['http://localhost:3000/add/1/1', 'http://localhost:3000/add/2/2', 'http://localhost:3000/add/3/3'],
{
initialState: 0,
}
if ( data ) {
console.log(typeof data); // number
}If initialState is an empty array then adding a simple type assertion will type the data type A
const { data } = useFetch(
['http://localhost:3000/add/1/1', 'http://localhost:3000/add/2/2', 'http://localhost:3000/add/3/3'],
{
initialState: [] as { answer: number }[],
}
if ( data ) {
for (const question of data) { // data is typed as array of { answer: number }
console.log(question.answer);
}
}TODO
- progress
- fetch client outside of react
- pagination
- load more
- graphql
- allow processing of results in order if required
- global configuration via context (still do not want to do this)
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago