From 5279add8444caabb9a2131767b33f11545a1ea52 Mon Sep 17 00:00:00 2001 From: Aaron Boodman Date: Tue, 12 Sep 2023 14:39:03 -1000 Subject: [PATCH] feat: Support more strongly-typed ReadTransaction. useSubscribe() is used by both Replicache and Reflect. Reflect has added a new form of ReadTransaction that has a generic get method. We would like users of replicache-react to be able to use that. However, currently replicache-react pulls in Replicache directly and uses its ReadTransaction. We could publish a new Replicache with the new signature, but then we would have a new problem: that head replicache-react only works with the latest Replicache/Reflect. Solution: Decouple replicache-react and Replicache completely by defining an abstract Subscribable. --- src/index.test.tsx | 9 +++++---- src/index.ts | 27 ++++++++++++++++----------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/index.test.tsx b/src/index.test.tsx index 7656caa..b4e843c 100644 --- a/src/index.test.tsx +++ b/src/index.test.tsx @@ -75,9 +75,10 @@ test('Batching of subscriptions', async () => { function A({rep}: {rep: MyRep}) { const dataA = useSubscribe( rep, - async tx => (await tx.get('a')) ?? null, + // TODO: Use type param to get when new Replicache is released. + async tx => ((await tx.get('a')) as string | undefined) ?? null, null, - ) as string | null; + ); renderLog.push('render A', dataA); return ; } @@ -85,9 +86,9 @@ test('Batching of subscriptions', async () => { function B({rep, dataA}: {rep: MyRep; dataA: string | null}) { const dataB = useSubscribe( rep, - async tx => (await tx.get('b')) ?? null, + async tx => ((await tx.get('b')) as string | undefined) ?? null, null, - ) as string | null; + ); renderLog.push('render B', dataA, dataB); return ( <> diff --git a/src/index.ts b/src/index.ts index 2d23a05..5452efb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,12 @@ -import type {Replicache} from 'replicache'; -import type {ReadonlyJSONValue, ReadTransaction} from 'replicache'; import {useEffect, useState} from 'react'; import {unstable_batchedUpdates} from 'react-dom'; -type Subscribable = Pick; +export type Subscribable = { + subscribe: ( + query: (tx: Tx) => Promise, + {onData}: {onData: (data: Data) => void}, + ) => () => void; +}; // 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. @@ -22,21 +25,23 @@ function doCallback() { }); } -export function useSubscribe( - rep: Subscribable | null | undefined, - query: (tx: ReadTransaction) => Promise, +export function useSubscribe( + r: Subscribable | null | undefined, + query: (tx: Tx) => Promise, def: R, deps: Array = [], ): R { const [snapshot, setSnapshot] = useState(def); useEffect(() => { - if (!rep) { + if (!r) { return; } - const unsubscribe = rep.subscribe(query, { - onData: (data: R) => { - callbacks.push(() => setSnapshot(data)); + const unsubscribe = r.subscribe(query, { + onData: data => { + // This is safe because we know that subscribe in fact can only return + // `R` (the return type of query or def). + callbacks.push(() => setSnapshot(data as R)); if (!hasPendingCallback) { void Promise.resolve().then(doCallback); hasPendingCallback = true; @@ -48,6 +53,6 @@ export function useSubscribe( unsubscribe(); setSnapshot(def); }; - }, [rep, ...deps]); + }, [r, ...deps]); return snapshot; }