UNPKG

11.3 kBMarkdownView Raw
1# React-async-hook
2
3[![NPM](https://img.shields.io/npm/dm/react-async-hook.svg)](https://www.npmjs.com/package/react-async-hook)
4[![Build Status](https://travis-ci.com/slorber/react-async-hook.svg?branch=master)](https://travis-ci.com/slorber/react-async-hook)
5
6This library only **does one small thing**, and **does it well**.
7
8Don't expect it to grow in size, because it is **feature complete**:
9
10- Handle fetches (`useAsync`)
11- Handle mutations (`useAsyncCallback`)
12- Handle cancellation (`useAsyncAbortable` + `AbortController`)
13- Handle race conditions
14- Platform agnostic
15- Works with any async function, not just backend API calls, not just fetch/axios...
16- Very good, native, Typescript support
17- Small, no dependency
18- Rules of hooks: ESLint find missing dependencies
19- Refetch on params change
20- Can trigger manual refetch
21- Options to customize state updates
22- Can mutate state after fetch
23
24## Small size
25
26- Tiny (1.5k minified gzipped)
27- At least 3 times smaller than popular alternatives.
28- CommonJS + ESM bundles, tree-shakable
29- Design using composition, good tree-shakeability
30
31**react-async-hook**:
32
33
34- ![](https://img.shields.io/bundlephobia/min/react-async-hook.svg)
35- ![](https://img.shields.io/bundlephobia/minzip/react-async-hook.svg)
36
37
38**React-Query**:
39
40- ![](https://img.shields.io/bundlephobia/min/react-query.svg)
41- ![](https://img.shields.io/bundlephobia/minzip/react-query.svg)
42
43
44**SWR**:
45
46- ![](https://img.shields.io/bundlephobia/min/swr.svg)
47- ![](https://img.shields.io/bundlephobia/minzip/swr.svg)
48
49
50## Things we don't support (by design):
51
52- stale-while-revalidate
53- refetch on focus / resume
54- caching
55- polling
56- request deduplication
57- platform-specific code
58- scroll position restoration
59- SSR
60- router integration for render-as-you-fetch pattern
61
62You can indeed build on top of this little lib to provide more advanced features, if you like composition, that is encouraged in the React ecosystem.
63
64If you prefer a full-featured fetching library, try [SWR](https://github.com/vercel/swr) or [React-Query](https://github.com/tannerlinsley/react-query).
65
66
67## Usecase: loading async data into a component
68
69The ability to inject remote/async data into a React component is a very common React need. Later we might support Suspense as well.
70
71```tsx
72import { useAsync } from 'react-async-hook';
73
74const fetchStarwarsHero = async id =>
75 (await fetch(`https://swapi.co/api/people/${id}/`)).json();
76
77const StarwarsHero = ({ id }) => {
78 const asyncHero = useAsync(fetchStarwarsHero, [id]);
79 return (
80 <div>
81 {asyncHero.loading && <div>Loading</div>}
82 {asyncHero.error && <div>Error: {asyncHero.error.message}</div>}
83 {asyncHero.result && (
84 <div>
85 <div>Success!</div>
86 <div>Name: {asyncHero.result.name}</div>
87 </div>
88 )}
89 </div>
90 );
91};
92```
93
94## Usecase: injecting async feedback into buttons
95
96If 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.
97
98Just wire `useAsyncCallback` to your `onClick` prop in your primitive `AppButton` component. The library will show a feedback only if the button onClick callback is async, otherwise it won't do anything.
99
100```tsx
101import { useAsyncCallback } from 'react-async-hook';
102
103const AppButton = ({ onClick, children }) => {
104 const asyncOnClick = useAsyncCallback(onClick);
105 return (
106 <button onClick={asyncOnClick.execute} disabled={asyncOnClick.loading}>
107 {asyncOnClick.loading ? '...' : children}
108 </button>
109 );
110};
111
112const CreateTodoButton = () => (
113 <AppButton
114 onClick={async () => {
115 await createTodoAPI('new todo text');
116 }}
117 >
118 Create Todo
119 </AppButton>
120);
121```
122
123# Examples
124
125Examples are running on [this page](https://react-async-hook.netlify.com/) and [implemented here](https://github.com/slorber/react-async-hook/blob/master/example/index.tsx) (in Typescript)
126
127# Install
128
129`yarn add react-async-hook`
130or
131
132`npm install react-async-hook --save`
133
134## ESLint
135
136If you use ESLint, use this [`react-hooks/exhaustive-deps`](https://github.com/facebook/react/blob/master/packages/eslint-plugin-react-hooks/README.md#advanced-configuration) setting:
137
138```ts
139// .eslintrc.js
140module.exports = {
141 // ...
142 rules: {
143 'react-hooks/rules-of-hooks': 'error',
144 'react-hooks/exhaustive-deps': [
145 'error',
146 {
147 additionalHooks: '(useAsync|useAsyncCallback)',
148 },
149 ],
150 }
151}
152```
153
154
155# FAQ
156
157#### How can I debounce the request
158
159It is possible to debounce a promise.
160
161I recommend [awesome-debounce-promise](https://github.com/slorber/awesome-debounce-promise), as it handles nicely potential concurrency issues and have React in mind (particularly the common usecase of a debounced search input/autocomplete)
162
163As debounced functions are stateful, we have to "store" the debounced function inside a component. We'll use for that [use-constant](https://github.com/Andarist/use-constant) (backed by `useRef`).
164
165```tsx
166const StarwarsHero = ({ id }) => {
167 // Create a constant debounced function (created only once per component instance)
168 const debouncedFetchStarwarsHero = useConstant(() =>
169 AwesomeDebouncePromise(fetchStarwarsHero, 1000)
170 );
171
172 // Simply use it with useAsync
173 const asyncHero = useAsync(debouncedFetchStarwarsHero, [id]);
174
175 return <div>...</div>;
176};
177```
178
179#### How can I implement a debounced search input / autocomplete?
180
181This is one of the most common usecase for fetching data + debouncing in a component, and can be implemented easily by composing different libraries.
182All this logic can easily be extracted into a single hook that you can reuse. Here is an example:
183
184```tsx
185const searchStarwarsHero = async (
186 text: string,
187 abortSignal?: AbortSignal
188): Promise<StarwarsHero[]> => {
189 const result = await fetch(
190 `https://swapi.co/api/people/?search=${encodeURIComponent(text)}`,
191 {
192 signal: abortSignal,
193 }
194 );
195 if (result.status !== 200) {
196 throw new Error('bad status = ' + result.status);
197 }
198 const json = await result.json();
199 return json.results;
200};
201
202const useSearchStarwarsHero = () => {
203 // Handle the input text state
204 const [inputText, setInputText] = useState('');
205
206 // Debounce the original search async function
207 const debouncedSearchStarwarsHero = useConstant(() =>
208 AwesomeDebouncePromise(searchStarwarsHero, 300)
209 );
210
211 const search = useAsyncAbortable(
212 async (abortSignal, text) => {
213 // If the input is empty, return nothing immediately (without the debouncing delay!)
214 if (text.length === 0) {
215 return [];
216 }
217 // Else we use the debounced api
218 else {
219 return debouncedSearchStarwarsHero(text, abortSignal);
220 }
221 },
222 // Ensure a new request is made everytime the text changes (even if it's debounced)
223 [inputText]
224 );
225
226 // Return everything needed for the hook consumer
227 return {
228 inputText,
229 setInputText,
230 search,
231 };
232};
233```
234
235And then you can use your hook easily:
236
237```tsx
238const SearchStarwarsHeroExample = () => {
239 const { inputText, setInputText, search } = useSearchStarwarsHero();
240 return (
241 <div>
242 <input value={inputText} onChange={e => setInputText(e.target.value)} />
243 <div>
244 {search.loading && <div>...</div>}
245 {search.error && <div>Error: {search.error.message}</div>}
246 {search.result && (
247 <div>
248 <div>Results: {search.result.length}</div>
249 <ul>
250 {search.result.map(hero => (
251 <li key={hero.name}>{hero.name}</li>
252 ))}
253 </ul>
254 </div>
255 )}
256 </div>
257 </div>
258 );
259};
260```
261
262#### How to use request cancellation?
263
264You can use the `useAsyncAbortable` alternative. The async function provided will receive `(abortSignal, ...params)` .
265
266The 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.
267It is your responsability to wire the abort signal appropriately.
268
269```tsx
270const StarwarsHero = ({ id }) => {
271 const asyncHero = useAsyncAbortable(
272 async (abortSignal, id) => {
273 const result = await fetch(`https://swapi.co/api/people/${id}/`, {
274 signal: abortSignal,
275 });
276 if (result.status !== 200) {
277 throw new Error('bad status = ' + result.status);
278 }
279 return result.json();
280 },
281 [id]
282 );
283
284 return <div>...</div>;
285};
286```
287
288#### How can I keep previous results available while a new request is pending?
289
290It can be annoying to have the previous async call result be "erased" everytime a new call is triggered (default strategy).
291If 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.
292It is possible to provide your own "merge" strategies.
293The following will ensure that on new calls, the previous result is kept until the new call result is received
294
295```tsx
296const StarwarsHero = ({ id }) => {
297 const asyncHero = useAsync(fetchStarwarsHero, [id], {
298 setLoading: state => ({ ...state, loading: true }),
299 });
300 return <div>...</div>;
301};
302```
303
304#### How to refresh / refetch the data?
305
306If your params are not changing, yet you need to refresh the data, you can call `execute()`
307
308```tsx
309const StarwarsHero = ({ id }) => {
310 const asyncHero = useAsync(fetchStarwarsHero, [id]);
311
312 return <div onClick={() => asyncHero.execute()}>...</div>;
313};
314```
315
316#### How to have better control when things get fetched/refetched?
317
318Sometimes 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.
319
320In this case you'd better use a closure with no arg define in the dependency array which params should trigger a refetch:
321
322Here, both `state.a` and `state.b` will trigger a refetch, despite b is not passed to the async fetch function.
323
324```tsx
325const asyncSomething = useAsync(() => fetchSomething(state.a), [state.a,state.b]);
326```
327
328Here, only `state.a` will trigger a refetch, despite b being passed to the async fetch function.
329
330```tsx
331const asyncSomething = useAsync(() => fetchSomething(state.a, state.b), [state.a]);
332```
333
334Note you can also use this to "build" a more complex payload. Using `useMemo` does not guarantee the memoized value will not be cleared, so it's better to do:
335
336```tsx
337const asyncSomething = useAsync(async () => {
338 const payload = buildFetchPayload(state);
339 const result = await fetchSomething(payload);
340 return result;
341}), [state.a, state.b, state.whateverNeedToTriggerRefetch]);
342```
343
344You can also use `useAsyncCallback` to decide yourself manually when a fetch should be done:
345
346```tsx
347const asyncSomething = useAsyncCallback(async () => {
348 const payload = buildFetchPayload(state);
349 const result = await fetchSomething(payload);
350 return result;
351}));
352
353// Call this manually whenever you need:
354asyncSomething.execute();
355```
356
357
358#### How to support retry?
359
360Use a lib that simply adds retry feature to async/promises directly. Doesn't exist? Build it.
361
362# License
363
364MIT
365
366# Hire a freelance expert
367
368Looking for a React/ReactNative freelance expert with more than 5 years production experience?
369Contact me from my [website](https://sebastienlorber.com/) or with [Twitter](https://twitter.com/sebastienlorber).