{"id":21186831,"url":"https://github.com/patrickroberts/suspense-service","last_synced_at":"2025-07-10T01:31:32.610Z","repository":{"id":57375032,"uuid":"288008350","full_name":"patrickroberts/suspense-service","owner":"patrickroberts","description":"Suspense integration library for React","archived":false,"fork":false,"pushed_at":"2021-12-07T09:11:13.000Z","size":654,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-10-28T22:56:28.532Z","etag":null,"topics":["airbnb","commonjs","es-module","eslint","javascript","jest","npm","react","react-suspense","typescript","umd"],"latest_commit_sha":null,"homepage":"https://patrickroberts.github.io/suspense-service","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/patrickroberts.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-08-16T19:12:13.000Z","updated_at":"2021-12-07T20:05:55.000Z","dependencies_parsed_at":"2022-09-05T14:11:23.964Z","dependency_job_id":null,"html_url":"https://github.com/patrickroberts/suspense-service","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickroberts%2Fsuspense-service","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickroberts%2Fsuspense-service/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickroberts%2Fsuspense-service/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickroberts%2Fsuspense-service/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/patrickroberts","download_url":"https://codeload.github.com/patrickroberts/suspense-service/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225609038,"owners_count":17496023,"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":["airbnb","commonjs","es-module","eslint","javascript","jest","npm","react","react-suspense","typescript","umd"],"created_at":"2024-11-20T18:26:26.361Z","updated_at":"2024-11-20T18:26:27.086Z","avatar_url":"https://github.com/patrickroberts.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# suspense-service\n\n[![build](https://badgen.net/github/checks/patrickroberts/suspense-service?icon=github\u0026label=build)](https://github.com/patrickroberts/suspense-service/actions)\n[![coverage](https://badgen.net/codecov/c/github/patrickroberts/suspense-service?icon=codecov\u0026label=coverage)](https://codecov.io/gh/patrickroberts/suspense-service)\n[![license](https://badgen.net/github/license/patrickroberts/suspense-service)](https://github.com/patrickroberts/suspense-service/blob/master/LICENSE)\n[![minzipped size](https://badgen.net/bundlephobia/minzip/suspense-service)][npm]\n[![tree shaking](https://badgen.net/bundlephobia/tree-shaking/suspense-service)][npm]\n[![types](https://badgen.net/npm/types/suspense-service?icon=typescript)][npm]\n[![version](https://badgen.net/npm/v/suspense-service?color=blue\u0026icon=npm\u0026label=version)][npm]\n\n[Suspense] integration library for [React]\n\n```jsx\nimport { Suspense } from 'react';\nimport { createService, useService } from 'suspense-service';\n\nconst myHandler = async (request) =\u003e {\n  const response = await fetch(request);\n  return response.json();\n};\n\nconst MyService = createService(myHandler);\n\nconst MyComponent = () =\u003e {\n  const data = useService(MyService);\n\n  return (\n    \u003cpre\u003e\n      {JSON.stringify(data, null, 2)}\n    \u003c/pre\u003e\n  );\n};\n\nconst App = () =\u003e (\n  \u003cMyService.Provider request=\"https://swapi.dev/api/planets/2/\"\u003e\n    \u003cSuspense fallback=\"Loading data...\"\u003e\n      \u003cMyComponent /\u003e\n    \u003c/Suspense\u003e\n  \u003c/MyService.Provider\u003e\n);\n```\n\n[![Edit suspense-service-demo](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-demo-sm9m5)\n\n## Why suspense-service?\n\nThis library aims to provide a generic integration between promise-based data fetching and React's Suspense API, eliminating much of the boilerplate associated with state management of asynchronous data. _Without Suspense, [data fetching often looks like this](https://reactjs.org/docs/concurrent-mode-suspense.html#approach-1-fetch-on-render-not-using-suspense)_:\n\n```jsx\nimport { useState, useEffect } from 'react';\n\nconst MyComponent = ({ request }) =\u003e {\n  const [data, setData] = useState();\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() =\u003e {\n    const fetchData = async (request) =\u003e {\n      const response = await fetch(request);\n      setData(await response.json());\n      setLoading(false);\n    };\n\n    fetchData(request);\n  }, [request]);\n\n  if (loading) {\n    return 'Loading data...';\n  }\n\n  return (\n    \u003cpre\u003e\n      {JSON.stringify(data, null, 2)}\n    \u003c/pre\u003e\n  );\n};\n\nconst App = () =\u003e (\n  \u003cMyComponent request=\"https://swapi.dev/api/planets/2/\" /\u003e\n);\n```\n\nThis may work well for trivial cases, but the amount of effort and code required tends to increase significantly for anything more advanced. Here are a few difficulities with this approach that `suspense-service` is intended to simplify.\n\n\u003cdetails\u003e\n\u003csummary\u003eAvoiding race conditions caused by out-of-order responses\u003c/summary\u003e\n\nAccomplishing this with the approach above would require additional logic to index each of the requests and compose a promise chain to ensure responses from older requests don't overwrite the current state when one from a more recent request is already available.\n\n[Concurrent Mode was designed to inherently solve this type of race condition using Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html#suspense-and-race-conditions).\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eProviding the response to one or more deeply nested components\u003c/summary\u003e\n\nThis would typically be done by passing the response down through props, or by creating a [Context] to provide the response. Both of these solutions would require a lot of effort, especially if you want to avoid re-rendering the intermediate components that aren't even using the response.\n\n`suspense-service` already creates an optimized context provider that allows the response to be consumed from multiple nested components without making multiple requests.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eMemoizing expensive computations based on the response\u003c/summary\u003e\n\nExpanding on the approach above, care would be needed in order to write a `useMemo()` that follows the [Rules of Hooks], and the expensive computation would need to be made conditional on the availability of `data` since it wouldn't be populated until a later re-render.\n\nWith `suspense-service`, you can simply pass `data` from `useService()` to `useMemo()`, and perform the computation unconditionally, because the component is suspended until the response is made available synchronously:\n\n```jsx\nconst MyComponent = () =\u003e {\n  const data = useService(MyService);\n  // some expensive computation\n  const formatted = useMemo(() =\u003e JSON.stringify(data, null, 2), [data]);\n\n  return (\n    \u003cpre\u003e\n      {formatted}\n    \u003c/pre\u003e\n  );\n};\n```\n\n[![Edit suspense-service-expensive-computation](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-expensive-computation-qmwi9)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eOther solved problems\u003c/summary\u003e\n\n[Concurrent Mode] introduces some UI patterns that were difficult to achieve with the existing approach. These patterns include [Transitions] and [Deferring a value].\n\u003c/details\u003e\n\n## Installing\n\nPackage available on [npm] or [Yarn]\n\n```bash\nnpm i suspense-service\nyarn add suspense-service\n```\n\n## Usage\n\n### `Service`\n\n\u003cdetails\u003e\n\u003csummary\u003eBasic Example\u003c/summary\u003e\n\n```jsx\nimport { Suspense } from 'react';\nimport { createService, useService } from 'suspense-service';\n\n/**\n * A user-defined service handler\n * It may accept a parameter of any type\n * but it must return a promise or thenable\n */\nconst myHandler = async (request) =\u003e {\n  const response = await fetch(request);\n  return response.json();\n};\n\n/**\n * A Service is like a Context\n * It contains a Provider and a Consumer\n */\nconst MyService = createService(myHandler);\n\nconst MyComponent = () =\u003e {\n  // Consumes MyService synchronously by suspending\n  // MyComponent until the response is available\n  const data = useService(MyService);\n\n  return \u003cpre\u003e{JSON.stringify(data, null, 2)}\u003c/pre\u003e;\n};\n\nconst App = () =\u003e (\n  // Fetch https://swapi.dev/api/people/1/\n  \u003cMyService.Provider request=\"https://swapi.dev/api/people/1/\"\u003e\n    {/* Render fallback while MyComponent is suspended */}\n    \u003cSuspense fallback=\"Loading data...\"\u003e\n      \u003cMyComponent /\u003e\n    \u003c/Suspense\u003e\n  \u003c/MyService.Provider\u003e\n);\n```\n\n[![Edit suspense-service-basic-example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-basic-example-oidy0)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eRender Callback\u003c/summary\u003e\n\n```jsx\nconst MyComponent = () =\u003e (\n  // Subscribe to MyService using a callback function\n  \u003cMyService.Consumer\u003e\n    {(data) =\u003e \u003cpre\u003e{JSON.stringify(data, null, 2)}\u003c/pre\u003e}\n  \u003c/MyService.Consumer\u003e\n);\n```\n\n[![Edit suspense-service-render-callback](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-render-callback-sf2tw)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eInline Suspense\u003c/summary\u003e\n\n```jsx\nconst App = () =\u003e (\n  // Passing the optional fallback prop\n  // wraps a Suspense around the children\n  \u003cMyService.Provider\n    request=\"https://swapi.dev/api/people/1/\"\n    fallback=\"Loading data...\"\n  \u003e\n    \u003cMyComponent /\u003e\n  \u003c/MyService.Provider\u003e\n);\n```\n\n[![Edit suspense-service-inline-suspense](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-inline-suspense-tf37k)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eMultiple Providers\u003c/summary\u003e\n\n```jsx\nconst MyComponent = () =\u003e {\n  // Specify which Provider to use\n  // by passing the optional id parameter\n  const a = useService(MyService, 'a');\n  const b = useService(MyService, 'b');\n\n  return \u003cpre\u003e{JSON.stringify({ a, b }, null, 2)}\u003c/pre\u003e;\n};\n\nconst App = () =\u003e (\n  // Identify each Provider with a key\n  // by using the optional id prop\n  \u003cMyService.Provider request=\"people/1/\" id=\"a\"\u003e\n    \u003cMyService.Provider request=\"people/2/\" id=\"b\"\u003e\n      \u003cSuspense fallback=\"Loading data...\"\u003e\n        \u003cMyComponent /\u003e\n      \u003c/Suspense\u003e\n    \u003c/MyService.Provider\u003e\n  \u003c/MyService.Provider\u003e\n);\n```\n\n[![Edit suspense-service-multiple-providers](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-multiple-providers-0o60m)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eMultiple Consumers\u003c/summary\u003e\n\n```jsx\nconst MyComponent = () =\u003e (\n  // Specify which Provider to use\n  // by passing the optional id parameter\n  \u003cMyService.Consumer id=\"a\"\u003e\n    {(a) =\u003e (\n      \u003cMyService.Consumer id=\"b\"\u003e\n        {(b) =\u003e \u003cpre\u003e{JSON.stringify({ a, b }, null, 2)}\u003c/pre\u003e}\n      \u003c/MyService.Consumer\u003e\n    )}\n  \u003c/MyService.Consumer\u003e\n);\n```\n\n[![Edit suspense-service-multiple-consumers](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-multiple-consumers-09ksg)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003ePagination\u003c/summary\u003e\n\n```jsx\nconst MyComponent = () =\u003e {\n  // Allows MyComponent to update MyService.Provider request\n  const [response, setRequest] = useServiceState(MyService);\n  const { previous: prev, next, results } = response;\n  const setPage = (page) =\u003e setRequest(page.replace(/^http:/, 'https:'));\n\n  return (\n    \u003c\u003e\n      \u003cbutton disabled={!prev} onClick={() =\u003e setPage(prev)}\u003e\n        Previous\n      \u003c/button\u003e\n      \u003cbutton disabled={!next} onClick={() =\u003e setPage(next)}\u003e\n        Next\n      \u003c/button\u003e\n      \u003cul\u003e\n        {results.map((result) =\u003e (\n          \u003cli key={result.url}\u003e\n            \u003ca href={result.url} target=\"_blank\" rel=\"noreferrer\"\u003e\n              {result.name}\n            \u003c/a\u003e\n          \u003c/li\u003e\n        ))}\n      \u003c/ul\u003e\n    \u003c/\u003e\n  );\n};\n```\n\n[![Edit suspense-service-pagination](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-pagination-v9so8)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eTransitions\u003c/summary\u003e\n\n\u003e Note that [Concurrent Mode] is required in order to enable [Transitions].\n\n```jsx\nconst MyComponent = () =\u003e {\n  // Allows MyComponent to update MyService.Provider request\n  const [response, setRequest] = useServiceState(MyService);\n  // Renders current response while next response is suspended\n  const [startTransition, isPending] = unstable_useTransition();\n  const { previous: prev, next, results } = response;\n  const setPage = (page) =\u003e {\n    startTransition(() =\u003e {\n      setRequest(page.replace(/^http:/, 'https:'));\n    });\n  };\n\n  return (\n    \u003c\u003e\n      \u003cbutton disabled={!prev || isPending} onClick={() =\u003e setPage(prev)}\u003e\n        Previous\n      \u003c/button\u003e{' '}\n      \u003cbutton disabled={!next || isPending} onClick={() =\u003e setPage(next)}\u003e\n        Next\n      \u003c/button\u003e\n      {isPending \u0026\u0026 'Loading next page...'}\n      \u003cul\u003e\n        {results.map((result) =\u003e (\n          \u003cli key={result.url}\u003e\n            \u003ca href={result.url} target=\"_blank\" rel=\"noreferrer\"\u003e\n              {result.name}\n            \u003c/a\u003e\n          \u003c/li\u003e\n        ))}\n      \u003c/ul\u003e\n    \u003c/\u003e\n  );\n};\n```\n\n[![Edit suspense-service-transitions](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-transitions-9h8tv)\n\n\u003c/details\u003e\n\n## Documentation\n\nAPI Reference available on [GitHub Pages]\n\n## Code Coverage\n\nAvailable on [Codecov](https://codecov.io/gh/patrickroberts/suspense-service)\n\n[Suspense]: https://reactjs.org/docs/concurrent-mode-suspense.html#what-is-suspense-exactly\n[React]: https://reactjs.org\n[Context]: https://reactjs.org/docs/context.html\n[Rules of Hooks]: https://reactjs.org/docs/hooks-rules.html\n[Concurrent Mode]: https://reactjs.org/docs/concurrent-mode-reference.html\n[Transitions]: https://reactjs.org/docs/concurrent-mode-patterns.html#transitions\n[Deferring a value]: https://reactjs.org/docs/concurrent-mode-patterns.html#deferring-a-value\n[npm]: https://www.npmjs.com/package/suspense-service\n[Yarn]: https://yarnpkg.com/package/suspense-service\n[GitHub Pages]: https://patrickroberts.github.io/suspense-service\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatrickroberts%2Fsuspense-service","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpatrickroberts%2Fsuspense-service","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatrickroberts%2Fsuspense-service/lists"}