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}