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 🙏

© 2024 – Pkg Stats / Ryan Hefner

api-read-hook

v0.1.2

Published

Simple API reads

Downloads

340

Readme

api-read-hook

npm version

Hook-based library for simple yet flexible data fetching and display in React apps.

  • Compatible with any data fetching backend (e.g. Fetch) and API format (e.g. REST, GraphQL).
  • Predictable default mode of operation: fresh data is always fetched when the component first mounts.
  • Pagination support (flexible enough for any server mechanism).
  • Manual invalidation controls to wipe out stale responses across all components, including automatic time-based invalidation.
  • Manual mutation controls to update responses across mounted components.
  • Configurable per-instance or via React context.
  • Full type-safety with TypeScript or Flow, including easy typing of expected responses.
  • Supports chaining API multiple dependent API requests.
  • Opt-in fine-grained control of a response cache, with clear identification of stale responses and their age.
  • TODO: A global cache, which can persist across screen unmounts.

Installation

$ npm install --save api-read-hook

# Or, with Yarn
$ yarn add api-read-hook

Getting started

import { ApiReadProvider, useApiRead } from 'api-read-hook';

/**
 * Build a specific reader using HTTP Fetch on a bespoke REST API
 */
async function myApiReader(path) {
  const response = await fetch(`https://example.com${path}`);
  if (!response.ok) throw new Error(`${path} returned ${response.status}`);
  const data = await response.json();
  return data;
}

function MyApp() {
  return (
    {/* Context configuration for all useApiRead calls */}
    <ApiReadProvider config={{ reader: myApiReader }}>
      <Navigation />
    </ApiReadProvider>
  );
}

function HomeScreen() {
  const { data, error } = useApiRead('/posts');

  if (error) return 'An error occurred!';
  if (!data) return 'Loading...';

  return (
    <ul>
      {data.map(post => (
        <Post key={post.id} post={post} />
      ))}
    </ul>
  );
}

Rationale

Compared with SWR

SWR is designed around (and even named after) the model of stale data being shown to the user by defualt, while new data fetches in the background.

In our view, this isn't desirable behaviour in the vast majority of cases for dynamic apps (particularly when considering React Native).

api-read-hook uses a simpler and more predictable model by default, where when a screen mounts, it always fetches fresh data, and the user sees a loading state.

This is in our opinion more intuitive for the user, as they can trust they're seeing the latest data every time they open a new screen, not worry the app may be opaquely digging up outdated stale data.

3 years after this library was created based on this justification, the situation seems unchanged. Vercel (SWR maintainers) are stil pushing ahead with their vision that data should be stale by default, despite much pushback: https://github.com/vercel/next.js/issues/42991#issuecomment-1569986363

API reference

reader function

The reader function is the only required piece of setup to make this library functional. It's how the library knows how to perform requests against your API (which could be REST, GraphQL, or any other type of protocol).

Your reader function, when provided with a string path (essentially a unique "key" for the request), should asynchronously perform the appropriate request against your API, carry out any processing on the response (e.g. parsing JSON), and returns it.

If the request fails, you should throw an Error. Feel free to extend Error adding as much extra metadata as you'd like to use in your app.

Configuration options

The library can be configured with the ReadConfig options, either as a second argument to useApiRead to change just a single instance, or in the top level ApiReadProvider config prop to set the defaults for all instances.

  • reader: (path: string) => Promise<mixed> - A reader function for your app's API, as described above.
  • staleWhileInvalidated?: boolean - If true, then when an API response is considered invalidated (either because you manually invalidated it, or it's become too old), the stale (invalidated) response will continue to be returned by the useApiRead hook.
  • staleWhileError?: boolean - If true, then when an API response returns an error, if there was a previous response returned by this hook, that will continue to be returned by the hook.
  • invalidateAge?: number - If provided, defines the number of seconds after which the current response will be marked as "invalidated", and therefore re-fetched. This is useful to ensure the user is never looking at content which is too old, even if they've kept the same screen open for a long time.

Read result

The useApiRead hook returns a ReadResult object. The following properties tell you about the data returned:

  • data: T | undefined - If the data hasn't been fetched yet, or has been invalidated without the staleWhileInvalidated option enabled, this will return undefined. You will want to check for the absence of data and use that to display a loading screen. Once the request succeeds, it will return the data from your reader function. In TypeScript/Flow, you can use generics (useApiRead<{ expected: string }>('/path')) to indicate the type T of this property.
  • error: Error | undefined - If the request failed, this can be used to display a relevant error message to the user.
  • stale: boolean - Will be set to true if the current data being returned is considered stale.
  • staleReason: null | 'invalidated' | 'error' - If a stale response is being returned (stale: true), indicates why.
  • receivedAt: null | number - If data is being returned, a unix timestamp (seconds) of when the data was received (i.e. when the reader returned the data).

The ReadResult also contains some functions which can be called to manipulate the current response:

  • invalidate: () => void - Call this when you know the current response is invalid and needs refetching (e.g. because the user just saved a change to the entity, and you want to re-fetch the authoritative state).
  • mutate: (data: T) => T - Make a direct change to the response payload (e.g. because the user just saved a change to the entity, which was simple enough that you can apply it directly).

Finally, ReadResult also contains some properties that can be used for pagination:

  • readMore: (path: string, updater: (moreData: T) => T) => void
  • loadingMore: boolean
  • moreError: Error | undefined

useInvalidation hook

Within any component, the useInvalidation hook can be used to retrieve helper functions for invalidating responses in other components:

  • invalidateExact: (search: string) => void - Searches through all currently held responses (i.e. any mounted hooks current displaying a response), and invalidates those where path is an exact match of search.
  • invalidateMatching: (search: string | RegExp) => void - Searchs through all currently held responses, and invalidates those where:
    • search is a string: path contains search.
    • search is a RegExp: path matches the search regexp.

useMutation hook

Works in a similar way to useInvalidation, but for mutation:

  • invalidateExact: (search: string, mutator: <T>(data: T) => T) => void
  • invalidateMatching: (search: string | RegExp, mutator: <T>(data: T) => T) => void

Examples

Path of one API call dependent on another

Apart from splitting the hooks across 2 components, and having the parent only render the child once its API call has succeeded, you can also approach this by passing null to path for the 2nd hook (which bypasses your reader) until you have enough information to construct the 2nd path:

function ProfileScreen({ name }: Props) {
  const apiSearch = useApiRead(`/find-user?name=${name}`);
  const apiUser = useApiRead(
    apiSearch.data ? `/profiles/${apiSearch.data.id}` : null
  );

  if (apiSearch.error || apiUser.error) return 'An error occurred!';
  if (!apiUser.data) return 'Loading...';
  const user = apiUser.data;

  return <h1>{user.name}</h1>;
}