npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

suspense-service

v0.3.2

Published

Suspense integration library for React

Downloads

59

Readme

suspense-service

build coverage license minzipped size tree shaking types version

Suspense integration library for React

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 (
    <pre>
      {JSON.stringify(data, null, 2)}
    </pre>
  );
};

const App = () => (
  <MyService.Provider request="https://swapi.dev/api/planets/2/">
    <Suspense fallback="Loading data...">
      <MyComponent />
    </Suspense>
  </MyService.Provider>
);

Edit suspense-service-demo

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:

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 (
    <pre>
      {JSON.stringify(data, null, 2)}
    </pre>
  );
};

const App = () => (
  <MyComponent request="https://swapi.dev/api/planets/2/" />
);

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.

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.

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.

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:

const MyComponent = () => {
  const data = useService(MyService);
  // some expensive computation
  const formatted = useMemo(() => JSON.stringify(data, null, 2), [data]);

  return (
    <pre>
      {formatted}
    </pre>
  );
};

Edit suspense-service-expensive-computation

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

npm i suspense-service
yarn add suspense-service

Usage

Service

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 <pre>{JSON.stringify(data, null, 2)}</pre>;
};

const App = () => (
  // Fetch https://swapi.dev/api/people/1/
  <MyService.Provider request="https://swapi.dev/api/people/1/">
    {/* Render fallback while MyComponent is suspended */}
    <Suspense fallback="Loading data...">
      <MyComponent />
    </Suspense>
  </MyService.Provider>
);

Edit suspense-service-basic-example

const MyComponent = () => (
  // Subscribe to MyService using a callback function
  <MyService.Consumer>
    {(data) => <pre>{JSON.stringify(data, null, 2)}</pre>}
  </MyService.Consumer>
);

Edit suspense-service-render-callback

const App = () => (
  // Passing the optional fallback prop
  // wraps a Suspense around the children
  <MyService.Provider
    request="https://swapi.dev/api/people/1/"
    fallback="Loading data..."
  >
    <MyComponent />
  </MyService.Provider>
);

Edit suspense-service-inline-suspense

const MyComponent = () => {
  // Specify which Provider to use
  // by passing the optional id parameter
  const a = useService(MyService, 'a');
  const b = useService(MyService, 'b');

  return <pre>{JSON.stringify({ a, b }, null, 2)}</pre>;
};

const App = () => (
  // Identify each Provider with a key
  // by using the optional id prop
  <MyService.Provider request="people/1/" id="a">
    <MyService.Provider request="people/2/" id="b">
      <Suspense fallback="Loading data...">
        <MyComponent />
      </Suspense>
    </MyService.Provider>
  </MyService.Provider>
);

Edit suspense-service-multiple-providers

const MyComponent = () => (
  // Specify which Provider to use
  // by passing the optional id parameter
  <MyService.Consumer id="a">
    {(a) => (
      <MyService.Consumer id="b">
        {(b) => <pre>{JSON.stringify({ a, b }, null, 2)}</pre>}
      </MyService.Consumer>
    )}
  </MyService.Consumer>
);

Edit suspense-service-multiple-consumers

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 (
    <>
      <button disabled={!prev} onClick={() => setPage(prev)}>
        Previous
      </button>
      <button disabled={!next} onClick={() => setPage(next)}>
        Next
      </button>
      <ul>
        {results.map((result) => (
          <li key={result.url}>
            <a href={result.url} target="_blank" rel="noreferrer">
              {result.name}
            </a>
          </li>
        ))}
      </ul>
    </>
  );
};

Edit suspense-service-pagination

Note that Concurrent Mode is required in order to enable Transitions.

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 (
    <>
      <button disabled={!prev || isPending} onClick={() => setPage(prev)}>
        Previous
      </button>{' '}
      <button disabled={!next || isPending} onClick={() => setPage(next)}>
        Next
      </button>
      {isPending && 'Loading next page...'}
      <ul>
        {results.map((result) => (
          <li key={result.url}>
            <a href={result.url} target="_blank" rel="noreferrer">
              {result.name}
            </a>
          </li>
        ))}
      </ul>
    </>
  );
};

Edit suspense-service-transitions

Documentation

API Reference available on GitHub Pages

Code Coverage

Available on Codecov