Created
May 27, 2022 21:08
Star
You must be signed in to star a gist
A react hook to use Suspense for data loading
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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