Skip to content

Instantly share code, notes, and snippets.

@dpeek
Created May 2, 2022 23:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dpeek/030270187e49de7116cad3ccf2ea030f to your computer and use it in GitHub Desktop.
Save dpeek/030270187e49de7116cad3ccf2ea030f to your computer and use it in GitHub Desktop.
useSubscribe that supports suspense
import { useEffect, useState } from 'react';
import { unstable_batchedUpdates } from 'react-dom';
import type {
ReadonlyJSONValue,
ReadTransaction,
Replicache,
} from 'replicache';
// We wrap all the callbacks in a `unstable_batchedUpdates` call to ensure that
// we do not render things more than once over all of the changed subscriptions.
let hasPendingCallback = false;
let callbacks: (() => void)[] = [];
function doCallback() {
const cbs = callbacks;
callbacks = [];
hasPendingCallback = false;
unstable_batchedUpdates(() => {
for (const callback of cbs) {
callback();
}
});
}
function wrapPromise<T>(promise: Promise<T>) {
let status = 'pending';
let result: any;
let suspender = promise.then(
(r: T) => {
status = 'success';
result = r;
},
(e: any) => {
status = 'error';
result = e;
}
);
return {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
} else if (status === 'success') {
return result;
}
},
};
}
const cache = new Map<string, any>();
function getSuspender<T>(
rep: Replicache,
query: (tx: ReadTransaction) => Promise<T>,
keys: Array<any>
) {
const key = JSON.stringify(keys);
const existing = cache.get(key);
if (existing) return existing;
console.log('new :(');
const suspender = wrapPromise(rep.query(query));
cache.set(key, suspender);
return suspender;
}
export function useSubscribe<R extends ReadonlyJSONValue>(
rep: Replicache,
query: (tx: ReadTransaction) => Promise<R>,
keys: Array<any> = []
): R {
const [snapshot, setSnapshot] = useState(() =>
getSuspender(rep, query, keys).read()
);
useEffect(() => {
return rep.subscribe(query, {
onData: (data: R) => {
if (data === snapshot) {
return;
}
callbacks.push(() => setSnapshot(data));
if (!hasPendingCallback) {
void Promise.resolve().then(doCallback);
hasPendingCallback = true;
}
},
});
}, [rep, query]);
return snapshot;
}
@arv
Copy link

arv commented May 3, 2022

I feel like you should be able to use useCallback and use the function instance in the map instead of stringifying the keys.

I still need to look deeper into suspense to get a better sense for how it is supposed to work.

@dpeek
Copy link
Author

dpeek commented May 3, 2022 via email

@MaxMusing
Copy link

Thanks for sharing @dpeek, this is really great.

Any reason you're not updating the cache in onData? With the current code, when a component has its initial render after the cache has been set for that key, it gets stale data.

Seems to be fixed if you add this after line 82:

const updatedSuspender = wrapPromise<QueryRet>(Promise.resolve(data));
cache.set(suspenseKey, updatedSuspender);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment