Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

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

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


>
);
};
```

[![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...'}


>
);
};
```

[![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