- Notifications
You must be signed in to change notification settings - Fork47
React hook to handle any async operation in React components, and prevent race conditions
slorber/react-async-hook
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Thistiny library onlydoes one thing, anddoes it well.
ThisWeekInReact.com: the best newsletter to stay up-to-date with the React ecosystem:
Don't expect it to grow in size, it isfeature complete:
- Handle fetches (
useAsync) - Handle mutations (
useAsyncCallback) - Handle cancellation (
useAsyncAbortable+AbortController) - Handlerace conditions
- Platform agnostic
- Works with any async function, not just backend API calls, not just fetch/axios...
- Very good, native, Typescript support
- Small, no dependency
- Rules of hooks: ESLint find missing dependencies
- Refetch on params change
- Can trigger manual refetch
- Options to customize state updates
- Can mutate state after fetch
- Returned callbacks are stable
- Way smaller than popular alternatives
- CommonJS + ESM bundles
- Tree-shakable
| Lib | min | min.gz |
|---|---|---|
| Suspend-React | ||
| React-Async-Hook | ||
| SWR | ||
| React-Query | ||
| React-Async | ||
| Use-HTTP | ||
| Rest-Hooks |
- stale-while-revalidate
- refetch on focus / resume
- caching
- polling
- request deduplication
- platform-specific code
- scroll position restoration
- SSR
- router integration for render-as-you-fetch pattern
You can build on top of this little lib to provide more advanced features (using composition), or move to popular full-featured libraries likeSWR orReact-Query.
The ability to inject remote/async data into a React component is a very common React need. Later we might support Suspense as well.
import{useAsync}from'react-async-hook';constfetchStarwarsHero=asyncid=>(awaitfetch(`https://swapi.dev/api/people/${id}/`)).json();constStarwarsHero=({ id})=>{constasyncHero=useAsync(fetchStarwarsHero,[id]);return(<div>{asyncHero.loading&&<div>Loading</div>}{asyncHero.error&&<div>Error:{asyncHero.error.message}</div>}{asyncHero.result&&(<div><div>Success!</div><div>Name:{asyncHero.result.name}</div></div>)}</div>);};
If you have a Todo app, you might want to show some feedback into the "create todo" button while the creation is pending, and prevent duplicate todo creations by disabling the button.
Just wireuseAsyncCallback to youronClick prop in your primitiveAppButton component. The library will show a feedback only if the button onClick callback is async, otherwise it won't do anything.
import{useAsyncCallback}from'react-async-hook';constAppButton=({ onClick, children})=>{constasyncOnClick=useAsyncCallback(onClick);return(<buttononClick={asyncOnClick.execute}disabled={asyncOnClick.loading}>{asyncOnClick.loading ?'...' :children}</button>);};constCreateTodoButton=()=>(<AppButtononClick={async()=>{awaitcreateTodoAPI('new todo text');}}> Create Todo</AppButton>);
Examples are running onthis page andimplemented here (in Typescript)
yarn add react-async-hookor
npm install react-async-hook --save
If you use ESLint, use thisreact-hooks/exhaustive-deps setting:
// .eslintrc.jsmodule.exports={// ...rules:{'react-hooks/rules-of-hooks':'error','react-hooks/exhaustive-deps':['error',{additionalHooks:'(useAsync|useAsyncCallback)',},],},};
It is possible to debounce a promise.
I recommendawesome-debounce-promise, as it handles nicely potential concurrency issues and have React in mind (particularly the common use-case of a debounced search input/autocomplete)
As debounced functions are stateful, we have to "store" the debounced function inside a component. We'll use for thatuse-constant (backed byuseRef).
constStarwarsHero=({ id})=>{// Create a constant debounced function (created only once per component instance)constdebouncedFetchStarwarsHero=useConstant(()=>AwesomeDebouncePromise(fetchStarwarsHero,1000));// Simply use it with useAsyncconstasyncHero=useAsync(debouncedFetchStarwarsHero,[id]);return<div>...</div>;};
This is one of the most common use-case for fetching data + debouncing in a component, and can be implemented easily by composing different libraries.All this logic can easily be extracted into a single hook that you can reuse. Here is an example:
constsearchStarwarsHero=async(text:string,abortSignal?:AbortSignal):Promise<StarwarsHero[]>=>{constresult=awaitfetch(`https://swapi.dev/api/people/?search=${encodeURIComponent(text)}`,{signal:abortSignal,});if(result.status!==200){thrownewError('bad status = '+result.status);}constjson=awaitresult.json();returnjson.results;};constuseSearchStarwarsHero=()=>{// Handle the input text stateconst[inputText,setInputText]=useState('');// Debounce the original search async functionconstdebouncedSearchStarwarsHero=useConstant(()=>AwesomeDebouncePromise(searchStarwarsHero,300));constsearch=useAsyncAbortable(async(abortSignal,text)=>{// If the input is empty, return nothing immediately (without the debouncing delay!)if(text.length===0){return[];}// Else we use the debounced apielse{returndebouncedSearchStarwarsHero(text,abortSignal);}},// Ensure a new request is made everytime the text changes (even if it's debounced)[inputText]);// Return everything needed for the hook consumerreturn{ inputText, setInputText, search,};};
And then you can use your hook easily:
constSearchStarwarsHeroExample=()=>{const{ inputText, setInputText, search}=useSearchStarwarsHero();return(<div><inputvalue={inputText}onChange={e=>setInputText(e.target.value)}/><div>{search.loading&&<div>...</div>}{search.error&&<div>Error:{search.error.message}</div>}{search.result&&(<div><div>Results:{search.result.length}</div><ul>{search.result.map(hero=>(<likey={hero.name}>{hero.name}</li>))}</ul></div>)}</div></div>);};
You can use theuseAsyncAbortable alternative. The async function provided will receive(abortSignal, ...params) .
The library will take care of triggering the abort signal whenever a new async call is made so that only the last request is not cancelled.It is your responsibility to wire the abort signal appropriately.
constStarwarsHero=({ id})=>{constasyncHero=useAsyncAbortable(async(abortSignal,id)=>{constresult=awaitfetch(`https://swapi.dev/api/people/${id}/`,{signal:abortSignal,});if(result.status!==200){thrownewError('bad status = '+result.status);}returnresult.json();},[id]);return<div>...</div>;};
It can be annoying to have the previous async call result be "erased" everytime a new call is triggered (default strategy).If you are implementing some kind of search/autocomplete dropdown, it means a spinner will appear everytime the user types a new char, giving a bad UX effect.It is possible to provide your own "merge" strategies.The following will ensure that on new calls, the previous result is kept until the new call result is received
constStarwarsHero=({ id})=>{constasyncHero=useAsync(fetchStarwarsHero,[id],{setLoading:state=>({ ...state,loading:true}),});return<div>...</div>;};
If your params are not changing, yet you need to refresh the data, you can callexecute()
constStarwarsHero=({ id})=>{constasyncHero=useAsync(fetchStarwarsHero,[id]);return<divonClick={()=>asyncHero.execute()}>...</div>;};
You can enable/disable the fetch logic directly inside the async callback. In some cases you know your API won't return anything useful.
constasyncSearchResults=useAsync(async()=>{// It's useless to call a search API with an empty textif(text.length===0){return[];}else{returngetSearchResultsAsync(text);}},[text]);
Sometimes you end up in situations where the function tries to fetch too often, or not often, because your dependency array changes and you don't know how to handle this.
In this case you'd better use a closure with no arg define in the dependency array which params should trigger a refetch:
Here, bothstate.a andstate.b will trigger a refetch, despite b is not passed to the async fetch function.
constasyncSomething=useAsync(()=>fetchSomething(state.a),[state.a,state.b,]);
Here, onlystate.a will trigger a refetch, despite b being passed to the async fetch function.
constasyncSomething=useAsync(()=>fetchSomething(state.a,state.b),[state.a,]);
Note you can also use this to "build" a more complex payload. UsinguseMemo does not guarantee the memoized value will not be cleared, so it's better to do:
constasyncSomething=useAsync(async()=>{constpayload=buildFetchPayload(state);constresult=awaitfetchSomething(payload);returnresult;}),[state.a,state.b,state.whateverNeedToTriggerRefetch]);
You can also useuseAsyncCallback to decide yourself manually when a fetch should be done:
constasyncSomething=useAsyncCallback(async()=>{constpayload=buildFetchPayload(state);constresult=awaitfetchSomething(payload);returnresult;}));// Call this manually whenever you need:asyncSomething.execute();
Use a lib that adds retry feature to async/promises directly.
MIT
Looking for a React/ReactNative freelance expert with more than 5 years production experience?Contact me from mywebsite or withTwitter.
About
React hook to handle any async operation in React components, and prevent race conditions
Topics
Resources
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors5
Uh oh!
There was an error while loading.Please reload this page.
