Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/patrickroberts/suspense-service
Suspense integration library for React
https://github.com/patrickroberts/suspense-service
airbnb commonjs es-module eslint javascript jest npm react react-suspense typescript umd
Last synced: 2 months ago
JSON representation
Suspense integration library for React
- Host: GitHub
- URL: https://github.com/patrickroberts/suspense-service
- Owner: patrickroberts
- License: mit
- Created: 2020-08-16T19:12:13.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2021-12-07T09:11:13.000Z (about 3 years ago)
- Last Synced: 2024-10-28T22:56:28.532Z (3 months ago)
- Topics: airbnb, commonjs, es-module, eslint, javascript, jest, npm, react, react-suspense, typescript, umd
- Language: TypeScript
- Homepage: https://patrickroberts.github.io/suspense-service
- Size: 639 KB
- Stars: 6
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# suspense-service
[![build](https://badgen.net/github/checks/patrickroberts/suspense-service?icon=github&label=build)](https://github.com/patrickroberts/suspense-service/actions)
[![coverage](https://badgen.net/codecov/c/github/patrickroberts/suspense-service?icon=codecov&label=coverage)](https://codecov.io/gh/patrickroberts/suspense-service)
[![license](https://badgen.net/github/license/patrickroberts/suspense-service)](https://github.com/patrickroberts/suspense-service/blob/master/LICENSE)
[![minzipped size](https://badgen.net/bundlephobia/minzip/suspense-service)][npm]
[![tree shaking](https://badgen.net/bundlephobia/tree-shaking/suspense-service)][npm]
[![types](https://badgen.net/npm/types/suspense-service?icon=typescript)][npm]
[![version](https://badgen.net/npm/v/suspense-service?color=blue&icon=npm&label=version)][npm][Suspense] integration library for [React]
```jsx
import { Suspense } from 'react';
import { createService, useService } from 'suspense-service';const myHandler = async (request) => {
const response = await fetch(request);
return response.json();
};const MyService = createService(myHandler);
const MyComponent = () => {
const data = useService(MyService);return (
{JSON.stringify(data, null, 2)}
);
};const App = () => (
);
```[![Edit suspense-service-demo](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-demo-sm9m5)
## Why suspense-service?
This 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)_:
```jsx
import { useState, useEffect } from 'react';const MyComponent = ({ request }) => {
const [data, setData] = useState();
const [loading, setLoading] = useState(true);useEffect(() => {
const fetchData = async (request) => {
const response = await fetch(request);
setData(await response.json());
setLoading(false);
};fetchData(request);
}, [request]);if (loading) {
return 'Loading data...';
}return (
{JSON.stringify(data, null, 2)}
);
};const App = () => (
);
```This 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.
Avoiding race conditions caused by out-of-order responses
Accomplishing 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.
[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).
Providing the response to one or more deeply nested components
This 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.
`suspense-service` already creates an optimized context provider that allows the response to be consumed from multiple nested components without making multiple requests.
Memoizing expensive computations based on the response
Expanding 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.
With `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:
```jsx
const MyComponent = () => {
const data = useService(MyService);
// some expensive computation
const formatted = useMemo(() => JSON.stringify(data, null, 2), [data]);return (
{formatted}
);
};
```[![Edit suspense-service-expensive-computation](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-expensive-computation-qmwi9)
Other solved problems
[Concurrent Mode] introduces some UI patterns that were difficult to achieve with the existing approach. These patterns include [Transitions] and [Deferring a value].
## Installing
Package available on [npm] or [Yarn]
```bash
npm i suspense-service
yarn add suspense-service
```## Usage
### `Service`
Basic Example
```jsx
import { Suspense } from 'react';
import { createService, useService } from 'suspense-service';/**
* A user-defined service handler
* It may accept a parameter of any type
* but it must return a promise or thenable
*/
const myHandler = async (request) => {
const response = await fetch(request);
return response.json();
};/**
* A Service is like a Context
* It contains a Provider and a Consumer
*/
const MyService = createService(myHandler);const MyComponent = () => {
// Consumes MyService synchronously by suspending
// MyComponent until the response is available
const data = useService(MyService);return
{JSON.stringify(data, null, 2)};
};const App = () => (
// Fetch https://swapi.dev/api/people/1/
{/* Render fallback while MyComponent is suspended */}
);
```[![Edit suspense-service-basic-example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-basic-example-oidy0)
Render Callback
```jsx
const MyComponent = () => (
// Subscribe to MyService using a callback function
{(data) =>{JSON.stringify(data, null, 2)}}
);
```[![Edit suspense-service-render-callback](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-render-callback-sf2tw)
Inline Suspense
```jsx
const App = () => (
// Passing the optional fallback prop
// wraps a Suspense around the children
);
```[![Edit suspense-service-inline-suspense](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-inline-suspense-tf37k)
Multiple Providers
```jsx
const MyComponent = () => {
// Specify which Provider to use
// by passing the optional id parameter
const a = useService(MyService, 'a');
const b = useService(MyService, 'b');return
{JSON.stringify({ a, b }, null, 2)};
};const App = () => (
// Identify each Provider with a key
// by using the optional id prop
);
```[![Edit suspense-service-multiple-providers](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-multiple-providers-0o60m)
Multiple Consumers
```jsx
const MyComponent = () => (
// Specify which Provider to use
// by passing the optional id parameter
{(a) => (
{(b) =>{JSON.stringify({ a, b }, null, 2)}}
)}
);
```[![Edit suspense-service-multiple-consumers](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-multiple-consumers-09ksg)
Pagination
```jsx
const MyComponent = () => {
// Allows MyComponent to update MyService.Provider request
const [response, setRequest] = useServiceState(MyService);
const { previous: prev, next, results } = response;
const setPage = (page) => setRequest(page.replace(/^http:/, 'https:'));return (
<>
setPage(prev)}>
Previous
setPage(next)}>
Next
-
{result.name}
{results.map((result) => (
))}
>
);
};
```
[![Edit suspense-service-pagination](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-pagination-v9so8)
Transitions
> Note that [Concurrent Mode] is required in order to enable [Transitions].
```jsx
const MyComponent = () => {
// Allows MyComponent to update MyService.Provider request
const [response, setRequest] = useServiceState(MyService);
// Renders current response while next response is suspended
const [startTransition, isPending] = unstable_useTransition();
const { previous: prev, next, results } = response;
const setPage = (page) => {
startTransition(() => {
setRequest(page.replace(/^http:/, 'https:'));
});
};
return (
<>
setPage(prev)}>
Previous
{' '}
setPage(next)}>
Next
{isPending && 'Loading next page...'}
-
{result.name}
{results.map((result) => (
))}
>
);
};
```
[![Edit suspense-service-transitions](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/suspense-service-transitions-9h8tv)
## Documentation
API Reference available on [GitHub Pages]
## Code Coverage
Available on [Codecov](https://codecov.io/gh/patrickroberts/suspense-service)
[Suspense]: https://reactjs.org/docs/concurrent-mode-suspense.html#what-is-suspense-exactly
[React]: https://reactjs.org
[Context]: https://reactjs.org/docs/context.html
[Rules of Hooks]: https://reactjs.org/docs/hooks-rules.html
[Concurrent Mode]: https://reactjs.org/docs/concurrent-mode-reference.html
[Transitions]: https://reactjs.org/docs/concurrent-mode-patterns.html#transitions
[Deferring a value]: https://reactjs.org/docs/concurrent-mode-patterns.html#deferring-a-value
[npm]: https://www.npmjs.com/package/suspense-service
[Yarn]: https://yarnpkg.com/package/suspense-service
[GitHub Pages]: https://patrickroberts.github.io/suspense-service