{"id":13847298,"url":"https://github.com/andreiduca/use-async-resource","last_synced_at":"2026-01-12T02:28:58.991Z","repository":{"id":39533075,"uuid":"259728764","full_name":"andreiduca/use-async-resource","owner":"andreiduca","description":"A custom React hook for simple data fetching with React Suspense","archived":false,"fork":false,"pushed_at":"2023-01-07T04:41:48.000Z","size":476,"stargazers_count":93,"open_issues_count":11,"forks_count":9,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-06-27T21:18:32.320Z","etag":null,"topics":["async","cache","custom-hook","data","data-fetching","fetch","hooks","react","react-cache","react-hook","react-hooks","react-suspense","reactjs","suspense"],"latest_commit_sha":null,"homepage":"https://dev.to/andreiduca/practical-implementation-of-data-fetching-with-react-suspense-that-you-can-use-today-273m","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andreiduca.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-04-28T19:13:36.000Z","updated_at":"2024-12-08T08:28:23.000Z","dependencies_parsed_at":"2023-02-06T12:01:27.746Z","dependency_job_id":null,"html_url":"https://github.com/andreiduca/use-async-resource","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/andreiduca/use-async-resource","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreiduca%2Fuse-async-resource","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreiduca%2Fuse-async-resource/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreiduca%2Fuse-async-resource/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreiduca%2Fuse-async-resource/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andreiduca","download_url":"https://codeload.github.com/andreiduca/use-async-resource/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreiduca%2Fuse-async-resource/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264962222,"owners_count":23689765,"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":["async","cache","custom-hook","data","data-fetching","fetch","hooks","react","react-cache","react-hook","react-hooks","react-suspense","reactjs","suspense"],"created_at":"2024-08-04T18:01:15.991Z","updated_at":"2026-01-12T02:28:58.985Z","avatar_url":"https://github.com/andreiduca.png","language":"TypeScript","readme":"# useAsyncResource - data fetching hook for React Suspense\n\nConvert any function that returns a Promise into a data reader function.\nThe data reader can then be consumed by a \"suspendable\" React component.\n\nThe hook also returns an updater handler that triggers new api calls.\nThe handler refreshes the data reader with each call.\n\n\n## ✨ Basic usage\n\n```\nyarn add use-async-resource\n```\n\nthen:\n\n```tsx\nimport { useAsyncResource } from 'use-async-resource';\n\n// a simple api function that fetches a user\nconst fetchUser = (id: number) =\u003e fetch(`.../get/user/by/${id}`).then(res =\u003e res.json());\n\nfunction App() {\n  // 👉 initialize the data reader and start fetching the user immediately\n  const [userReader, getNewUser] = useAsyncResource(fetchUser, 1);\n\n  return (\n    \u003c\u003e\n      \u003cErrorBoundary\u003e\n        \u003cReact.Suspense fallback=\"user is loading...\"\u003e\n          \u003cUser userReader={userReader} /* 👈 pass it to a suspendable child component */ /\u003e\n        \u003c/React.Suspense\u003e\n      \u003c/ErrorBoundary\u003e\n      \u003cbutton onClick={() =\u003e getNewUser(2)}\u003eGet user with id 2\u003c/button\u003e\n      {/* clicking the button 👆 will start fetching a new user */}\n    \u003c/\u003e\n  );\n}\n\nfunction User({ userReader }) {\n  const userData = userReader(); // 😎 just call the data reader function to get the user object\n\n  return \u003cdiv\u003e{userData.name}\u003c/div\u003e;\n}\n```\n\n\n### Data Reader and Refresh handler\n\nThe `useAsyncResource` hook returns a pair:\n- the **data reader function**, which returns the expected result, or throws if the result is not yet available;\n- a **refresh handler to fetch new data** with new parameters.\n\nThe returned data reader `userReader` is a function that returns the user object if the api call completed successfully.\n\nIf the api call has not finished, the data reader function throws the promise, which is caught by the `React.Suspense` boundary.\nSuspense will retry to render the child component until it's successful, meaning the promised completed, the data is available, and the data reader doesn't throw anymore.\n\nIf the api call fails with an error, that error is thrown, and the `ErrorBoundary` component will catch it.\n\nThe refresh handler is identical with the original wrapped function, except it doesn't return anything - it only triggers new api calls.\nThe data is retrievable with the data reader function.\n\nNotice the returned items are a pair, so you can name them whatever you want, using the array destructuring:\n\n```tsx\nconst [userReader, getUser] = useAsyncResource(fetchUser, id);\n\nconst [postsReader, getPosts] = useAsyncResource(fetchPosts, category);\n\nconst [commentsReader, getComments] = useAsyncResource(fetchPostComments, postId, { orderBy: \"date\", order: \"desc\" });\n```\n\n\n### Api functions that don't accept parameters\n\nIf the api function doesn't accept any parameters, just pass an empty array as the second argument:\n\n```tsx\nconst fetchToggles = () =\u003e fetch('/path/to/global/toggles').then(res =\u003e res.json());\n\n// in App.jsx\nconst [toggles] = useAsyncResource(fetchToggles, []);\n```\n\nJust like before, the api call is immediately invoked and the `toggles` data reader can be passed to a suspendable child component.\n\n\n## 🦥 Lazy initialization\n\nAll of the above examples are eagerly initialized, meaning the data starts fetching as soon as the `useAsyncResource` is called.\nBut in some cases you would want to start fetching data only after a user interaction.\n\nTo lazily initialize the data reader, just pass the api function without any parameters:\n\n```tsx\nconst [userReader, getUserDetails] = useAsyncResource(fetchUserDetails);\n```\n\nThen use the refresh handler to start fetching data when needed:\n\n```tsx\nconst [selectedUserId, setUserId] = React.useState();\n\nconst selectUserHandler = React.useCallback((userId) =\u003e {\n  setUserId(userId);\n  getUserDetails(userId); // 👈 call the refresh handler to trigger new api calls\n}, []);\n\nreturn (\n  \u003c\u003e\n    \u003cUsersList onUserItemClick={selectUserHandler} /\u003e\n    {selectedUserId \u0026\u0026 (\n      \u003cReact.Suspense\u003e\n        \u003cUserDetails userReader={userReader} /\u003e\n      \u003c/React.Suspense\u003e\n    )}\n  \u003c/\u003e\n);\n```\n\nThe only difference between a lazy data reader and an eagerly initialized one is that\nthe lazy data reader can also return `undefined` if the data fetching hasn't stared yet.\n\nBe aware of this difference when consuming the data in the child component:\n\n```tsx\nfunction UserDetails({ userReader }) {\n  const userData = userReader();\n  // 👆 this may be `undefined` at first, so we need to check for it\n\n  if (userData === undefined) {\n    return null;\n  }\n\n  return \u003cdiv\u003e{userData.username} - {userData.email}\u003c/div\u003e\n}\n```\n\n\n## 📦 Resource caching\n\nAll resources are cached, so subsequent calls with the same parameters for the same api function\nreturn the same resource, and don't trigger new, identical api calls.\n\nThis is useful for many reasons. First, it means you don't have to necessarily initialize the data reader\nin a parent component. You only have to wrap the child component in a Suspense boundary:\n\n```tsx\nfunction App() {\n  return (\n    \u003cReact.Suspense fallback=\"loading posts\"\u003e\n      \u003cPosts /\u003e\n    \u003c/React.Suspense\u003e\n  );\n}\n\nfunction Posts(props) {\n  // as usual, initialize the data reader and start fetching the posts\n  const [postsReader] = useAsyncResource(fetchPosts, []);\n\n  // now read the posts and render a list\n  const postsList = postsReader();\n\n  return postsList.map(post =\u003e \u003cPost post={post} /\u003e);\n}\n```\n\nThis still works as you'd expect, even if the `App` component re-renders for any other reason,\nbefore, during or even after the posts have loaded. Because the data reader gets cached, only the first initialization will trigger an api call.\n\n\nThis also means you can write code like this, without having to think about deduplicating requests for the same user id:\n\n```tsx\nfunction App() {\n  // just like before, start fetching posts\n  const [postsReader] = useAsyncResource(fetchPosts, []);\n\n  return (\n    \u003cReact.Suspense fallback=\"loading posts\"\u003e\n      \u003cPosts dataReader={postsReader} /\u003e\n    \u003c/React.Suspense\u003e\n  );\n}\n\n\nfunction Posts(props) {\n  // read the posts and render a list\n  const postsList = props.dataReader();\n\n  return postsList.map(post =\u003e \u003cPost post={post} /\u003e);\n}\n\n\nfunction Post(props) {\n  // start fetching users for each individual post\n  const [userReader] = useAsyncResource(fetchUser, props.post.authorId);\n  // 👉 notice we don't need to deduplicate the user resource for potentially identical author ids\n\n  return (\n    \u003carticle\u003e\n      \u003ch1\u003e{props.post.title}\u003c/h1\u003e\n      \u003cReact.Suspense fallback=\"loading author\"\u003e\n        \u003cAuthor dataReader={userReader} /\u003e\n      \u003c/React.Suspense\u003e\n      \u003cp\u003e{props.post.body}\u003c/p\u003e\n    \u003c/article\u003e\n  );\n}\n\n\nfunction Author(props) {\n  // get the user object as usual\n  const user = props.dataReader();\n\n  return \u003cdiv\u003e{user.displayName}\u003c/div\u003e;\n}\n```\n\n\n### 🚚 Preloading resources\n\nWhen you know a resource will be consumed by a child component, you can preload it ahead of time.\nThis is useful in cases such as lazy loaded components, or when trying to predict a user's intent.\n\n```tsx\n// 👉 import the `preloadResource` helper\nimport { useAsyncResource, preloadResource } from 'use-async-resource';\n\n// a lazy-loaded React component\nconst PostsList = React.lazy(() =\u003e import('./PostsListComponent'));\n\n// some api function\nconst fetchUserPosts = (userId) =\u003e fetch(`/path/to/get/user/${userId}/posts`).then(res =\u003e res.json())\n\n\nfunction UserProfile(props) {\n  const [showPostsList, toggleList] = React.useState(false);\n\n  return (\n    \u003c\u003e\n      \u003ch1\u003e{props.user.name}\u003c/h1\u003e\n      \u003cbutton\n        // show the list on button click\n        onClick={() =\u003e toggleList(true)}\n        // 👉 we can preload the resource as soon as the user\n        // shows any intent of interacting with the button \n        onMouseOver={() =\u003e preloadResource(fetchUserPosts, props.user.id)}\n      \u003e\n        show user posts\n      \u003c/button\u003e\n\n      {showPostsList \u0026\u0026 (\n        // this child will suspend if either:\n        // - the `PostList` component code is not yet loaded\n        // - or the data reader inside it is not yet ready\n        // 👉 notice we're not initializing any resource to pass it to the child component\n        \u003cReact.Suspense fallback=\"...loading posts\"\u003e\n          \u003cPostsList userId={props.user.id} /\u003e\n        \u003c/React.Suspense\u003e  \n      )}\n    \u003c/\u003e\n  );\n}\n\n// in PostsListComponent.tsx\nfunction PostsList(props) {\n  // 👉 instead, we initialize the data reader inside the child component directly\n  const [posts] = useAsyncResource(fetchUserPosts, props.userId);\n\n  // ✨ because we preloaded it in the parent with the same `userId` parameter,\n  // it will get initialized with that cached version\n\n  // also, the outer React.Suspense boundary in the parent will take care of rendering the fallback\n  return (\n    \u003cul\u003e\n      {posts().map(post =\u003e \u003cli\u003e\u003cPost post={post} /\u003e\u003c/li\u003e)}\n    \u003c/ul\u003e\n  );\n}\n```\n\nIn the above example, even if the child component loads faster than the data,\nre-rendering it multiple times until the data is ready is ok, because every time\nthe data reader will be initialized from the same cached version.\nNo api call will ever be triggered from the child component,\nbecause that happened in the parent when the user hovered the button.\n\nAt the same time, if the data is ready before the code loads, it will be available immediately\nwhen the child component will render for the first time. \n\n\n### Clearing caches\n\nFinally, you can manually clear caches by using the `resourceCache` helper.\n\n```tsx\nimport { useAsyncResource, resourceCache } from 'use-async-resource';\n\n// ...\n\nconst [latestPosts, getPosts] = useAsyncResource(fetchLatestPosts, []);\n\nconst refreshLatestPosts = React.useCallback(() =\u003e {\n  // 🧹 clear the cache so we can make a new api call\n  resourceCache(fetchLatestPosts).clear();\n  // 🙌 refresh the data reader\n  getPosts();\n}, []);\n```\n\nIn this case, we're clearing the entire cache for the `fetchLatestPosts` api function.\nBut you can also use the `delete()` method with parameters, so you only delete the cache for those specific ones:\n\n```tsx\nconst [user, getUser] = useAsyncResource(fetchUser, id);\n\nconst refreshUserProfile = React.useCallback((userId) =\u003e {\n  // only clear the cache for that id\n  resourceCache(fetchUser).delete(userId);\n  // get new user data\n  getUser(userId);\n}, []);\n```\n\n\n## Data modifiers\n\nWhen consumed, the data reader can take an optional argument: a function to modify the data.\nThis function receives the original data as a parameter, and the transformation logic is up to you.\n\n```tsx\nconst userDisplayName = userDataReader(user =\u003e `${user.firstName} ${user.lastName}`);\n```\n\n\n## File resource helpers\n\nSuspense is not just about fetching data in a declarative way, but about fetching resources in general, including images and scripts.\n\nThe included `fileResource` helper will turn a URL string into a resource \"data reader\" function, but it will load a resource instead of data.\nWhen the resource finishes loading, the \"data reader\" function will return the URL you passed in. Until then, it will throw a Promise, so Suspense can render a fallback. \n\nHere's an example for an image resource:\n\n```tsx\nimport { useAsyncResource, fileResource } from 'use-async-resource';\n\nfunction Author({ user }) {\n  // initialize the image \"data reader\"\n  const [userImageReader] = useAsyncResource(fileResource.image, user.profilePicUrl);\n    \n  return (\n    \u003carticle\u003e\n      {/* render a fallback until the image is downloaded */}\n      \u003cReact.Suspense fallback={\u003cSomeImgPlaceholder /\u003e}\u003e\n        {/* pass the resource \"data reader\" to a suspendable component */}\n        \u003cProfilePhoto resource={userImageReader} /\u003e\n      \u003c/React.Suspense\u003e\n      \u003ch1\u003e{user.name}\u003c/h1\u003e\n      \u003ch2\u003e{user.bio}\u003c/h2\u003e\n    \u003c/article\u003e\n  );\n}\n\nfunction ProfilePhoto(props) {\n  // just read back the URL and use it in an `img` tag when the image is ready\n  const imageSrc = props.resource();\n\n  return \u003cimg src={imageSrc} /\u003e;\n}\n```\n\nUsing the `fileResource` to load external scripts is just as easy:\n\n```tsx\nfunction App() {\n  const [jq] = useAsyncResource(fileResource.script, 'https://code.jquery.com/jquery-3.4.1.slim.min.js');\n\n  return (\n    \u003cReact.Suspense fallback=\"jQuery loading...\"\u003e\n      \u003cJQComponent jQueryResource={jq} /\u003e\n    \u003c/React.Suspense\u003e\n  );\n}\n\nfunction JQComponent(props) {\n  const jQ = props.jQueryResource();\n\n  // jQuery should be available and you can do something with it\n  return \u003cdiv\u003ejQuery version: {window.jQuery.fn.jquery}\u003c/div\u003e\n}\n```\n\nNotice we don’t do anything with the `const jQ`, but we still need to call `props.jQueryResource()` so it can throw,\nrendering the fallback until the library is fully loaded on the page.\n\n\n## 📘 TypeScript support\n\nThe `useAsyncResource` hook infers all types from the api function passed in.\nThe arguments it accepts after the api function are exactly the parameters of the original api function.\n\n```tsx\nconst fetchUser = (userId: number): Promise\u003cUserType\u003e =\u003e fetch('...');\n\nconst [wrongUserReader] = useAsyncResource(fetchUser, \"some\", \"string\", \"params\"); // 🚨 TS will complain about this\nconst [correctUserReader] = useAsyncResource(fetchUser, 1); // 👌 just right\nconst [lazyUserReader] = useAsyncResource(fetchUser); // 🦥 also ok, but lazily initialized\n```\n\nThe only exception is the api function without parameters:\n- the hook doesn't accept any other arguments than the api function, meaning it's lazily initialized;\n- or it accepts a single extra argument, an empty array, when we want the resource to start loading immediately.\n\n```tsx\nconst [lazyToggles] = useAsyncResource(fetchToggles); // 🦥 ok, but lazily initialized\nconst [eagerToggles] = useAsyncResource(fetchToggles, []); // 🚀 ok, starts fetching immediately\nconst [wrongToggles] = useAsyncResource(fetchToggles, \"some\", \"params\"); // 🚨 TS will complain about this\n```\n\n\n### Type inference for the data reader\n\nThe data reader will return exactly the type the original api function returns as a Promise.\n\n```tsx\nconst fetchUser = (userId: number): Promise\u003cUserType\u003e =\u003e fetch('...');\n\nconst [userReader] = useAsyncResource(fetchUser, 1);\n```\n\n`userReader` is inferred as `() =\u003e UserType`, meaning a `function` that returns a `UserType` object.\n\nIf the resource is lazily initialized, the `userReader` can also return `undefined`:\n\n```tsx\nconst [userReader] = useAsyncResource(fetchUser);\n```\n\nHere, `userReader` is inferred as `() =\u003e (UserType | undefined)`, meaning a `function` that returns either a `UserType` object, or `undefined`.\n\n\n### Type inference for the refresh handler\n\nNot just the data reader types are inferred, but also the arguments of the refresh handler:\n\n```tsx\nconst fetchUser = (userId: number): Promise\u003cUserType\u003e =\u003e fetch('...');\n\nconst [userReader, getNewUser] = useAsyncResource(fetchUser, 1);\n```\n\nThe `getNewUser` handler is inferred as `(userId: number) =\u003e void`, meaning a `function` that takes a numeric argument `userId`, but doesn't return anything.\n\nRemember: the return type of the handler is always `void`, because the handler only kicks off new data api calls.\nThe data is still retrievable via the data reader function.\n\n\n## Default Suspense and ErrorBoundary wrappers\n\nAgain, a component consuming a data reader needs to be wrapped in both a `React.Suspense` boundary and a custom `ErrorBoundary`.\n\nFor convenience, you can use the bundled `AsyncResourceContent` that provides both:\n\n```tsx\nimport { useAsyncResource, AsyncResourceContent } from 'use-async-resource';\n\n// ...\n\n\u003cAsyncResourceContent\n  fallback=\"loading your data...\"\n  errorMessage=\"Some generic message when bad things happen\"\n\u003e\n  \u003cSomeComponent consuming={aDataReader} /\u003e\n\u003c/AsyncResourceContent\u003e\n```\n\nThe `fallback` can be a `string` or a React component.\n\nThe `errorMessage` can be either a `string`, a React component,\nor a function that takes the thrown error as an argument and returns a `string` or a React component.\n\n```tsx\n\u003cAsyncResourceContent\n  fallback={\u003cSpinner /\u003e}\n  errorMessage={(e: CustomErrorType) =\u003e \u003cspan style={{ color: 'red' }}\u003e{e.message}\u003c/span\u003e}\n\u003e\n  \u003cSomeComponent consuming={aDataReader} /\u003e\n\u003c/AsyncResourceContent\u003e\n```\n\n\n### Custom Error Boundary\n\nOptionally, you can pass a custom error boundary component to be used instead of the default one:\n\n```tsx\nclass MyCustomErrorBoundary extends React.Component { ... }\n\n// ...\n\n\u003cAsyncResourceContent\n  // ...\n  errorComponent={MyCustomErrorBoundary}\n  errorMessage={/* optional error message */}\n\u003e\n  \u003cSomeComponent consuming={aDataReader} /\u003e\n\u003c/AsyncResourceContent\u003e\n```\n\nIf you also pass the `errorMessage` prop, your custom error boundary will receive it as a prop.\n","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandreiduca%2Fuse-async-resource","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandreiduca%2Fuse-async-resource","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandreiduca%2Fuse-async-resource/lists"}