a tool for shared writing and social publishing
1"use client"; 2import { DependencyList, useEffect, useState } from "react"; 3 4export type Subscribable<Tx> = { 5 subscribe<Data>( 6 query: (tx: Tx) => Promise<Data>, 7 options: { 8 onData: (data: Data) => void; 9 isEqual?: ((a: Data, b: Data) => boolean) | undefined; 10 }, 11 ): () => void; 12}; 13 14// We wrap all the callbacks in a `unstable_batchedUpdates` call to ensure that 15// we do not render things more than once over all of the changed subscriptions. 16 17let hasPendingCallback = false; 18let callbacks: (() => void)[] = []; 19 20function doCallback() { 21 const cbs = callbacks; 22 callbacks = []; 23 hasPendingCallback = false; 24 for (const callback of cbs) { 25 callback(); 26 } 27} 28 29export type RemoveUndefined<T> = T extends undefined ? never : T; 30 31export type UseSubscribeOptions<QueryRet, Default> = { 32 /** Default can already be undefined since it is an unbounded type parameter. */ 33 default?: Default; 34 dependencies?: DependencyList | undefined; 35 isEqual?: ((a: QueryRet, b: QueryRet) => boolean) | undefined; 36}; 37 38/** 39 * Runs a query and returns the result. Re-runs automatically whenever the 40 * query changes. 41 * 42 * NOTE: Changing `r` will cause the query to be re-run, but changing `query` 43 * or `options` will not (by default). This is by design because these two 44 * values are often object/array/function literals which change on every 45 * render. If you want to re-run the query when these change, you can pass 46 * them as dependencies. 47 */ 48export function useSubscribe<Tx, QueryRet, Default = undefined>( 49 r: Subscribable<Tx> | null | undefined, 50 query: (tx: Tx) => Promise<QueryRet>, 51 options: UseSubscribeOptions<QueryRet, Default> = {}, 52): RemoveUndefined<QueryRet> | Default { 53 const { default: def, dependencies = [], isEqual } = options; 54 const [snapshot, setSnapshot] = useState<QueryRet | undefined>(undefined); 55 useEffect(() => { 56 if (!r) { 57 return; 58 } 59 60 const unsubscribe = r.subscribe(query, { 61 onData: (data) => { 62 // This is safe because we know that subscribe in fact can only return 63 // `R` (the return type of query or def). 64 callbacks.push(() => setSnapshot(data)); 65 if (!hasPendingCallback) { 66 void Promise.resolve().then(doCallback); 67 hasPendingCallback = true; 68 } 69 }, 70 isEqual, 71 }); 72 73 return () => { 74 unsubscribe(); 75 setSnapshot(undefined); 76 }; 77 }, [r, ...dependencies]); 78 if (snapshot === undefined) { 79 return def as Default; 80 } 81 return snapshot as RemoveUndefined<QueryRet>; 82}