Skip to content

Instantly share code, notes, and snippets.

@jsg2021
Created May 27, 2022 21:08
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save jsg2021/ae0bcee740584fb77e6cf1d550e3a646 to your computer and use it in GitHub Desktop.
A react hook to use Suspense for data loading
import { useEffect } from 'react';
/**
* @template T
* @typedef {{ read: () => T }} Reader
*/
const DATA = {
/** @type {Object<string, Reader<any>>} */
objects: {},
};
/**
* @template {any} T
* @param {() => Promise<T>} factory The factory makes the request, and returns the results. Its the factory's responsibility to manage/cancel inflight re-entry.
* @param {string} key
* @param {*} reload - if set, will reload once per unique instance (no primitives allowed)
* @returns {T}
*/
export function useAsyncValue(factory, key, reload) {
let reader = DATA.objects[key];
if (shouldReload(reload) || !reader) {
reader = DATA.objects[key] = toReader(factory());
// we initialize to 0 because we will increment within the effect hook.
reader.used = reader?.used ?? 0;
}
useEffect(
// This effect just increments the usage count, and returns a cleanup call.
() => {
// This will only increment each time this body is called (once per component that is using the
// key/reader instance) If the key changes, the cleanup will decrement and eventually free/delete
// the data.
reader.used++;
// The cleanup will decrement the used count and if zero,
// remove it so we get a fresh object next time. (but only
// if the object matches what we have)
return () => {
if (--reader.used <= 0 && reader === DATA.objects[key]) {
delete DATA.objects[key];
}
};
},
// effect only runs on mount/unmount with an empty dep list.
[key, reader],
);
return reader.read();
}
function shouldReload(nonce) {
const self = shouldReload;
const seen = self.seen || (self.seen = new WeakSet());
if (/boolean|string|number/.test(typeof nonce)) {
throw new Error('Reload nonce should be an object with a unique reference (address). Primitive values are non-unique.');
}
if (nonce && !seen.has(nonce)) {
seen.add(nonce);
return true;
}
return false;
}
/**
* This is the Suspense wrapper for async fetching/resolving.
* Use this to make a reader so that you can read a value as
* if it were sync.
*
* @template T
* @param {Promise<T>} promise
* @returns {Reader<T>}
*/
export function toReader(promise) {
let status = 'pending';
let result;
const suspender = promise.then(
r => {
status = 'success';
result = r;
},
e => {
status = 'error';
result = e;
}
);
return {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
} else if (status === 'success') {
return result;
}
},
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment