Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

async-ref

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

async-ref

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

  • 0.1.4
  • npm
  • Socket score

Version published
Maintainers
1
Created
Source

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

Keywords

FAQs

Package last updated on 14 Apr 2022

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc