replicache-react
replicache-react copied to clipboard
Support suspense in useSubscribe
Because of the async nature of Replicache queries, useSubscribe currently has this type:
function useSubscribe<R>(rep:Replicache, query:(tx: ReadTransaction) => Promise<R>, def:R, deps:Array<any>):R {}
Where def allows users to provide a fallback value until the first query completes. In many cases there is no sensible value for users to provide here, meaning they fallback to null checking the results throughout their app:
function useDocument(rep:Replicache, id:string):Doc | null {
return useSubscribe(cache, (tx) => {/*...*/}, null, []);
}
function SomeComponent({rep, id}:{rep:Replicache, id:string}) {
let doc = useDocument(rep, id);
if (!doc) return null;
return <div>{doc.title}</div>
}
This can be avoiding by making useSubscribe support suspense. Basically the hook should throw a Promise that resolves once the first value is returned by the query.
Users can then wrap their useSubscribe components in a SuspenseBoundary that will suspend until all queries have returned a value, and no more null checks \o/
One thing to bear in mind while implementing is that suspending a component (aka throwing a promise in the render function) nukes all state for that component, so you need to maintain state outside of React.
Seems reasonable.
I'm new to suspense, would useSubscribe be called again when the promise resolves? If so we need some book keeping to ensure we do not end up with multiple subscriptions.
@arv the component re-renders when the promise resolves, so the hook gets called again then. But then, hooks get called all the time during render. I tried implementing this myself and the documentation on suspense is a bit lacking right now!
Calling the hook multiple times is OK normally because the subscription gets unregistered. But if the code throws it is not clear how React is going to be able to unregister the subscription as needed.
I think I understand what you mean. Maybe we can use the existing code for subscription, and suspense would only be used for the first value?
As you say, I can't think of how you would cleanup the subscription if the suspended component was unmounted (maybe this is possible with class component?)
So the hook would throw a promise that resolves when first value is ready, then useEffect would setup subscription as it does now. If the component had unmounted in the meantime I guess useEffect just wouldn't get called?
@arv I took yet another stab at figuring this out. I'm not completely happy with the result but it's heading in the right direction: https://gist.github.com/dpeek/030270187e49de7116cad3ccf2ea030f
The problem I hit is that you need some key to store the thrown promise against, as React refuses to store any state for a suspended component tree. My naive solution right now is just an array of keys that get JSON serialised, kind of like react-query does.
On first render the initial value for snapshot is read from a suspender cached on that key. The suspender wraps the replicache query promise and throws as it hasn't resolved yet. When the query resolves we resolve the suspender, at which point the component tree re-renders, snapshot gets the cached+resolved suspender and the initial value is set / returned by the hook.
The existing subscription stuff remains as is, and just gets thrown away on first render. I compare against snapshot to avoid a second redundant render.
It kind of sucks requiring a key like this, but I'm not sure how else to do it, maybe you have some ideas :)
I think the react-query approach of using a string or array/object of things serializable to a string, is the right one.
The only other approach I can think of is to use the identity of the query function as a key. This requires the calling component to memoize the query function using useCallback/useMemo. If they don't, the component will keep throwing to the Suspense boundary every time it renders. This can also happen even if you do memoize the query function, if the dependencies array itself has a member that changes every render. It just seems to open the door to nasty bugs that involve debugging the user's application instead of this library.
Using a string key also means users can leverage the libraries that help manage your keys, lint them, etc. In other words, this approach seems to have "won" for solving this problem and there's a big advantage to walking a paved road.
Lastly, we have our own custom useSubscribe that supports suspense using a string key and it works great. Everyone who's used react-query or a similar lib immediately understands it. And if they don't, we link them to the React Query docs on it...