{"id":13429539,"url":"https://github.com/slorber/react-async-hook","last_synced_at":"2025-05-14T20:06:07.223Z","repository":{"id":38751588,"uuid":"154992215","full_name":"slorber/react-async-hook","owner":"slorber","description":"React hook to handle any async operation in React components, and prevent race conditions","archived":false,"fork":false,"pushed_at":"2023-07-08T12:55:35.000Z","size":1693,"stargazers_count":1185,"open_issues_count":31,"forks_count":45,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-04-13T14:06:15.576Z","etag":null,"topics":["hooks"],"latest_commit_sha":null,"homepage":"https://sebastienlorber.com/handling-api-request-race-conditions-in-react","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/slorber.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-10-27T17:41:29.000Z","updated_at":"2025-04-08T18:22:23.000Z","dependencies_parsed_at":"2024-06-18T12:37:02.099Z","dependency_job_id":null,"html_url":"https://github.com/slorber/react-async-hook","commit_stats":{"total_commits":109,"total_committers":6,"mean_commits":"18.166666666666668","dds":"0.47706422018348627","last_synced_commit":"9351dab27b4e42623b9acdacbf3524ef220035b1"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slorber%2Freact-async-hook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slorber%2Freact-async-hook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slorber%2Freact-async-hook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slorber%2Freact-async-hook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/slorber","download_url":"https://codeload.github.com/slorber/react-async-hook/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248724639,"owners_count":21151561,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hooks"],"created_at":"2024-07-31T02:00:41.462Z","updated_at":"2025-04-13T14:06:20.159Z","avatar_url":"https://github.com/slorber.png","language":"TypeScript","readme":"# React-Async-Hook\n\n[![NPM](https://img.shields.io/npm/dm/react-async-hook.svg)](https://www.npmjs.com/package/react-async-hook)\n[![Build Status](https://travis-ci.com/slorber/react-async-hook.svg?branch=master)](https://travis-ci.com/slorber/react-async-hook)\n\nThis **tiny** library only **does one thing**, and **does it well**.\n\n--- \n\n# Sponsor\n\n**[ThisWeekInReact.com](https://thisweekinreact.com/react-async-hook)**: the best newsletter to stay up-to-date with the React ecosystem:\n\n[![ThisWeekInReact.com banner](https://user-images.githubusercontent.com/749374/136185889-ebdb67cd-ec78-4655-b88b-79a6c134acd2.png)](https://thisweekinreact.com/react-async-hook)\n\n---\n\nDon't expect it to grow in size, it is **feature complete**:\n\n- Handle fetches (`useAsync`)\n- Handle mutations (`useAsyncCallback`)\n- Handle cancellation (`useAsyncAbortable` + `AbortController`)\n- Handle [race conditions](https://sebastienlorber.com/handling-api-request-race-conditions-in-react)\n- Platform agnostic\n- Works with any async function, not just backend API calls, not just fetch/axios...\n- Very good, native, Typescript support\n- Small, no dependency\n- Rules of hooks: ESLint find missing dependencies\n- Refetch on params change\n- Can trigger manual refetch\n- Options to customize state updates\n- Can mutate state after fetch\n- Returned callbacks are stable\n\n\n\n## Small size\n\n- Way smaller than popular alternatives\n- CommonJS + ESM bundles\n- Tree-shakable\n\n| Lib                  | min                                                                                                                    | min.gz                                                                                                                    |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |\n| **Suspend-React** | [![](https://img.shields.io/bundlephobia/min/suspend-react.svg)](https://bundlephobia.com/package/suspend-react) | [![](https://img.shields.io/bundlephobia/minzip/suspend-react.svg)](https://bundlephobia.com/package/suspend-react) |\n| **React-Async-Hook** | [![](https://img.shields.io/bundlephobia/min/react-async-hook.svg)](https://bundlephobia.com/package/react-async-hook) | [![](https://img.shields.io/bundlephobia/minzip/react-async-hook.svg)](https://bundlephobia.com/package/react-async-hook) |\n| **SWR**              | [![](https://img.shields.io/bundlephobia/min/swr.svg)](https://bundlephobia.com/package/swr)                           | [![](https://img.shields.io/bundlephobia/minzip/swr.svg)](https://bundlephobia.com/package/swr)                           |\n| **React-Query**      | [![](https://img.shields.io/bundlephobia/min/react-query.svg)](https://bundlephobia.com/package/react-query)           | [![](https://img.shields.io/bundlephobia/minzip/react-query.svg)](https://bundlephobia.com/package/react-query)           |\n| **React-Async**      | [![](https://img.shields.io/bundlephobia/min/react-async.svg)](https://bundlephobia.com/package/react-async)           | [![](https://img.shields.io/bundlephobia/minzip/react-async.svg)](https://bundlephobia.com/package/react-async)           |\n| **Use-HTTP**         | [![](https://img.shields.io/bundlephobia/min/use-http.svg)](https://bundlephobia.com/package/use-http)                 | [![](https://img.shields.io/bundlephobia/minzip/use-http.svg)](https://bundlephobia.com/package/use-http)                 |\n| **Rest-Hooks**       | [![](https://img.shields.io/bundlephobia/min/rest-hooks.svg)](https://bundlephobia.com/package/rest-hooks)             | [![](https://img.shields.io/bundlephobia/minzip/rest-hooks.svg)](https://bundlephobia.com/package/rest-hooks)             |\n\n---\n\n## Things we don't support (by design):\n\n- stale-while-revalidate\n- refetch on focus / resume\n- caching\n- polling\n- request deduplication\n- platform-specific code\n- scroll position restoration\n- SSR\n- router integration for render-as-you-fetch pattern\n\nYou can build on top of this little lib to provide more advanced features (using composition), or move to popular full-featured libraries like [SWR](https://github.com/vercel/swr) or [React-Query](https://github.com/tannerlinsley/react-query).\n\n## Use-case: loading async data into a component\n\nThe ability to inject remote/async data into a React component is a very common React need. Later we might support Suspense as well.\n\n```tsx\nimport { useAsync } from 'react-async-hook';\n\nconst fetchStarwarsHero = async id =\u003e\n  (await fetch(`https://swapi.dev/api/people/${id}/`)).json();\n\nconst StarwarsHero = ({ id }) =\u003e {\n  const asyncHero = useAsync(fetchStarwarsHero, [id]);\n  return (\n    \u003cdiv\u003e\n      {asyncHero.loading \u0026\u0026 \u003cdiv\u003eLoading\u003c/div\u003e}\n      {asyncHero.error \u0026\u0026 \u003cdiv\u003eError: {asyncHero.error.message}\u003c/div\u003e}\n      {asyncHero.result \u0026\u0026 (\n        \u003cdiv\u003e\n          \u003cdiv\u003eSuccess!\u003c/div\u003e\n          \u003cdiv\u003eName: {asyncHero.result.name}\u003c/div\u003e\n        \u003c/div\u003e\n      )}\n    \u003c/div\u003e\n  );\n};\n```\n\n## Use-case: injecting async feedback into buttons\n\nIf 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.\n\nJust 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.\n\n```tsx\nimport { useAsyncCallback } from 'react-async-hook';\n\nconst AppButton = ({ onClick, children }) =\u003e {\n  const asyncOnClick = useAsyncCallback(onClick);\n  return (\n    \u003cbutton onClick={asyncOnClick.execute} disabled={asyncOnClick.loading}\u003e\n      {asyncOnClick.loading ? '...' : children}\n    \u003c/button\u003e\n  );\n};\n\nconst CreateTodoButton = () =\u003e (\n  \u003cAppButton\n    onClick={async () =\u003e {\n      await createTodoAPI('new todo text');\n    }}\n  \u003e\n    Create Todo\n  \u003c/AppButton\u003e\n);\n```\n\n# Examples\n\nExamples 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)\n\n# Install\n\n`yarn add react-async-hook`\nor\n\n`npm install react-async-hook --save`\n\n## ESLint\n\nIf 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:\n\n```ts\n// .eslintrc.js\nmodule.exports = {\n  // ...\n  rules: {\n    'react-hooks/rules-of-hooks': 'error',\n    'react-hooks/exhaustive-deps': [\n      'error',\n      {\n        additionalHooks: '(useAsync|useAsyncCallback)',\n      },\n    ],\n  },\n};\n```\n\n# FAQ\n\n#### How can I debounce the request\n\nIt is possible to debounce a promise.\n\nI 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 use-case of a debounced search input/autocomplete)\n\nAs 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`).\n\n```tsx\nconst StarwarsHero = ({ id }) =\u003e {\n  // Create a constant debounced function (created only once per component instance)\n  const debouncedFetchStarwarsHero = useConstant(() =\u003e\n    AwesomeDebouncePromise(fetchStarwarsHero, 1000)\n  );\n\n  // Simply use it with useAsync\n  const asyncHero = useAsync(debouncedFetchStarwarsHero, [id]);\n\n  return \u003cdiv\u003e...\u003c/div\u003e;\n};\n```\n\n#### How can I implement a debounced search input / autocomplete?\n\nThis is one of the most common use-case for fetching data + debouncing in a component, and can be implemented easily by composing different libraries.\nAll this logic can easily be extracted into a single hook that you can reuse. Here is an example:\n\n```tsx\nconst searchStarwarsHero = async (\n  text: string,\n  abortSignal?: AbortSignal\n): Promise\u003cStarwarsHero[]\u003e =\u003e {\n  const result = await fetch(\n    `https://swapi.dev/api/people/?search=${encodeURIComponent(text)}`,\n    {\n      signal: abortSignal,\n    }\n  );\n  if (result.status !== 200) {\n    throw new Error('bad status = ' + result.status);\n  }\n  const json = await result.json();\n  return json.results;\n};\n\nconst useSearchStarwarsHero = () =\u003e {\n  // Handle the input text state\n  const [inputText, setInputText] = useState('');\n\n  // Debounce the original search async function\n  const debouncedSearchStarwarsHero = useConstant(() =\u003e\n    AwesomeDebouncePromise(searchStarwarsHero, 300)\n  );\n\n  const search = useAsyncAbortable(\n    async (abortSignal, text) =\u003e {\n      // If the input is empty, return nothing immediately (without the debouncing delay!)\n      if (text.length === 0) {\n        return [];\n      }\n      // Else we use the debounced api\n      else {\n        return debouncedSearchStarwarsHero(text, abortSignal);\n      }\n    },\n    // Ensure a new request is made everytime the text changes (even if it's debounced)\n    [inputText]\n  );\n\n  // Return everything needed for the hook consumer\n  return {\n    inputText,\n    setInputText,\n    search,\n  };\n};\n```\n\nAnd then you can use your hook easily:\n\n```tsx\nconst SearchStarwarsHeroExample = () =\u003e {\n  const { inputText, setInputText, search } = useSearchStarwarsHero();\n  return (\n    \u003cdiv\u003e\n      \u003cinput value={inputText} onChange={e =\u003e setInputText(e.target.value)} /\u003e\n      \u003cdiv\u003e\n        {search.loading \u0026\u0026 \u003cdiv\u003e...\u003c/div\u003e}\n        {search.error \u0026\u0026 \u003cdiv\u003eError: {search.error.message}\u003c/div\u003e}\n        {search.result \u0026\u0026 (\n          \u003cdiv\u003e\n            \u003cdiv\u003eResults: {search.result.length}\u003c/div\u003e\n            \u003cul\u003e\n              {search.result.map(hero =\u003e (\n                \u003cli key={hero.name}\u003e{hero.name}\u003c/li\u003e\n              ))}\n            \u003c/ul\u003e\n          \u003c/div\u003e\n        )}\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n};\n```\n\n#### How to use request cancellation?\n\nYou can use the `useAsyncAbortable` alternative. The async function provided will receive `(abortSignal, ...params)` .\n\nThe 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.\nIt is your responsibility to wire the abort signal appropriately.\n\n```tsx\nconst StarwarsHero = ({ id }) =\u003e {\n  const asyncHero = useAsyncAbortable(\n    async (abortSignal, id) =\u003e {\n      const result = await fetch(`https://swapi.dev/api/people/${id}/`, {\n        signal: abortSignal,\n      });\n      if (result.status !== 200) {\n        throw new Error('bad status = ' + result.status);\n      }\n      return result.json();\n    },\n    [id]\n  );\n\n  return \u003cdiv\u003e...\u003c/div\u003e;\n};\n```\n\n#### How can I keep previous results available while a new request is pending?\n\nIt can be annoying to have the previous async call result be \"erased\" everytime a new call is triggered (default strategy).\nIf 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.\nIt is possible to provide your own \"merge\" strategies.\nThe following will ensure that on new calls, the previous result is kept until the new call result is received\n\n```tsx\nconst StarwarsHero = ({ id }) =\u003e {\n  const asyncHero = useAsync(fetchStarwarsHero, [id], {\n    setLoading: state =\u003e ({ ...state, loading: true }),\n  });\n  return \u003cdiv\u003e...\u003c/div\u003e;\n};\n```\n\n#### How to refresh / refetch the data?\n\nIf your params are not changing, yet you need to refresh the data, you can call `execute()`\n\n```tsx\nconst StarwarsHero = ({ id }) =\u003e {\n  const asyncHero = useAsync(fetchStarwarsHero, [id]);\n\n  return \u003cdiv onClick={() =\u003e asyncHero.execute()}\u003e...\u003c/div\u003e;\n};\n```\n\n#### How to handle conditional fetch?\n\nYou can enable/disable the fetch logic directly inside the async callback. In some cases you know your API won't return anything useful.\n\n```tsx\nconst asyncSearchResults = useAsync(async () =\u003e {\n  // It's useless to call a search API with an empty text\n  if (text.length === 0) {\n    return [];\n  } else {\n    return getSearchResultsAsync(text);\n  }\n}, [text]);\n```\n\n#### How to have better control when things get fetched/refetched?\n\nSometimes 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.\n\nIn this case you'd better use a closure with no arg define in the dependency array which params should trigger a refetch:\n\nHere, both `state.a` and `state.b` will trigger a refetch, despite b is not passed to the async fetch function.\n\n```tsx\nconst asyncSomething = useAsync(() =\u003e fetchSomething(state.a), [\n  state.a,\n  state.b,\n]);\n```\n\nHere, only `state.a` will trigger a refetch, despite b being passed to the async fetch function.\n\n```tsx\nconst asyncSomething = useAsync(() =\u003e fetchSomething(state.a, state.b), [\n  state.a,\n]);\n```\n\nNote 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:\n\n```tsx\nconst asyncSomething = useAsync(async () =\u003e {\n  const payload = buildFetchPayload(state);\n  const result = await fetchSomething(payload);\n  return result;\n}), [state.a, state.b, state.whateverNeedToTriggerRefetch]);\n```\n\nYou can also use `useAsyncCallback` to decide yourself manually when a fetch should be done:\n\n```tsx\nconst asyncSomething = useAsyncCallback(async () =\u003e {\n  const payload = buildFetchPayload(state);\n  const result = await fetchSomething(payload);\n  return result;\n}));\n\n// Call this manually whenever you need:\nasyncSomething.execute();\n```\n\n#### How to support retry?\n\nUse a lib that adds retry feature to async/promises directly.\n\n# License\n\nMIT\n\n# Hire a freelance expert\n\nLooking for a React/ReactNative freelance expert with more than 5 years production experience?\nContact me from my [website](https://sebastienlorber.com/) or with [Twitter](https://twitter.com/sebastienlorber).\n","funding_links":[],"categories":["Packages","Asynchronous operations","TypeScript","Debouncing a search input (with React hooks)","React"],"sub_categories":["React Components"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fslorber%2Freact-async-hook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fslorber%2Freact-async-hook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fslorber%2Freact-async-hook/lists"}