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

async-ref

v0.1.6

Published

Async ref objects for React. A tiny bridge between React.useSyncExternalStore and React.Suspense.

Downloads

14,218

Readme

A tiny bridge between React.useSyncExternalStore and React Suspense

This library allows you to create asynchronous ref objects (AsyncMutableRefObject<T>). Similar to the sync ref objects you'd get back from useRef<T>(), except if the current value of the ref (ref.current) is not set, it will throw a Promise instead of returning nothing.

React.Suspense works by catching promises—thrown as part of rendering a component—and waiting for them to resolve before re-rendering again. Assigning a value to ref.current will trigger the suspense boundary to re-render. (You can read more about how Suspense works in the React docs.)

Because ref.current will either return a value T or throw a promise, the only thing it can return is a T and therefore implements the MutableRefObject<T> interface. That is,

type MutableRefObject<T> = {
  current: T;
};

class AsyncMutableRefObject<T> implements MutableRefObject<T> {
  // ...
}

React v18 introduces the experimental hook useSyncExternalStore which provides a convenient way to hook into synchronous external data sources.

declare function useSyncExternalStore<Snapshot>(
  subscribe: (onStoreChange: () => void) => () => void,
  getSnapshot: () => Snapshot,
  getServerSnapshot?: () => Snapshot
): Snapshot;

AsyncMutableRefObject<T> makes it easy for a typed asynchronous external data source to work with React Suspense in a typed synchornous way because we can implement getSnapshot to just always return the result of ref.current.

Example

A sample implementation of what you might do is the following where we define a data source that implements getSnapshot and subscribe functions.

type Subscriber = () => void;

class AsyncDataStore<T> {
  subscribers = new Set<Subscriber>();

  ref = createAsyncRef<T>(() => this.notify());

  getSnapshot = (): T => {
    return this.ref.current;
  };

  subscribe = (sub: Subscriber): Subscriber => {
    this.subscribers.add(sub);
    return () => this.subscribers.delete(sub);
  };

  notify() {
    this.subscribers.forEach((sub) => sub());
  }

  doSomething() {
    fetch(/*...*/)
      .then((res) => res.json())
      .then((data) => {
        // setting the current value will notify all subscribers.
        ref.current = data;
      });
  }
}

As long as getSnapshot() is called from within the React-render cycle, the Promise it (might) throw will be caught by Suspense.

const store = new AsyncDataStore<User>();

// ...

const user = React.useSyncExternalStore(store.subscribe, store.getSnapshot);
// => throws Promise from the ref,
//    which is caught by a Suspense boundary
//    that waits for it to resolve.

// ...

store.doSomething();
// => after some time, the current value is set.

// ...

const user = React.useSyncExternalStore(store.subscribe, store.getSnapshot);
// => returns a User

Use

async-ref exports a single function createAsyncRef<T>() which accepts an optional notifier function and returns an AsyncMutableRefObject<T>.

declare function createAsyncRef<T>(notifier?: () => void): AsyncMutableRefObject<T>;
import { createAsyncRef } from "async-ref";

const ref = createAsyncRef<User>();

const currentValue: User = ref.current;
// => throws a Promise

ref.current = { id: "12345", name: "Gabe" /* etc */ };

const currentValue: User = ref.current;
// => returns { id: '12345', name: 'Gabe', /* etc */ }

Just like a MutableRefObject, the current value can be set any number of times.

import { createAsyncRef } from "async-ref";

const ref = createAsyncRef<number>();

ref.current = 12;
ref.current = 400;

const currentValue = ref.current;
// => 400

Alternatively, AsyncMutableRefObject<T> exposes resolve/reject functions for a more familiar Promise-type feel.

import { createAsyncRef } from "async-ref";

const ref = createAsyncRef<number>();

ref.reject(new Error("uh oh!"));

ref.current;
// => throws an Error("uh oh!")

If you provide a notifier function, it will be called every time the state of the ref changes.

import { createAsyncRef } from "async-ref";

const listener = jest.fn();

const ref = createAsyncRef<number>(listener);

ref.current = 12;
ref.current = 400;

expect(listener).toHaveBeenCallTimes(2);

If you want to prevent the ref from changing its state, you can freeze it.

const ref = createAsyncRef<number>(listener);

ref.current = 12;
ref.freeze();

ref.current = 400;

expect(ref.current).toEqual(12);

Safely getting the value without Suspense

AsyncMutableRefObject<T> also implements PromiseLike<T> which means that you can dereference the current value by awaiting on it. (If a current value is already set, it will immediately resolve.) This is safer than calling ref.current because it will wait for a current value to be set before resolving the promise, but of course does not work inside of a React component because it is asynchronous.

import { createAsyncRef } from "async-ref";

const ref = createAsyncRef<User>(listener);

// ...

const user = await ref;

Installing

yarn add async-ref