Skip to content

Commit 5f493cd

Browse files
authored
feat(store-sync): add react provider and hook (#3451)
1 parent 16242b7 commit 5f493cd

File tree

14 files changed

+274
-65
lines changed

14 files changed

+274
-65
lines changed

.changeset/lucky-goats-sell.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"@latticexyz/store-sync": patch
3+
---
4+
5+
Added an experimental `@latticexyz/store-sync/react` export with a `SyncProvider` and `useSync` hook. This allows for easier syncing MUD data to React apps.
6+
7+
Note that this is currently only usable with Stash and assumes you are also using Wagmi in your React app.
8+
9+
```tsx
10+
import { WagmiProvider } from "wagmi";
11+
import { QueryClientProvider } from "@tanstack/react-query";
12+
import { SyncProvider } from "@latticexyz/store-sync/react";
13+
import { createSyncAdapter } from "@latticexyz/store-sync/internal";
14+
15+
export function App() {
16+
return (
17+
<WagmiProvider config={wagmiConfig}>
18+
<QueryClientProvider client={queryClient}>
19+
<SyncProvider
20+
chainId={chainId}
21+
address={worldAddress}
22+
startBlock={startBlock}
23+
adapter={createSyncAdapter({ stash })}
24+
>
25+
{children}
26+
</SyncProvider>
27+
</QueryClientProvider>
28+
</WagmiProvider>
29+
);
30+
}
31+
```

packages/store-sync/package.json

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
".": "./dist/index.js",
1414
"./indexer-client": "./dist/indexer-client/index.js",
1515
"./internal": "./dist/exports/internal.js",
16+
"./react": "./dist/exports/react.js",
1617
"./postgres": "./dist/postgres/index.js",
1718
"./postgres-decoded": "./dist/postgres-decoded/index.js",
1819
"./recs": "./dist/recs/index.js",
@@ -32,6 +33,9 @@
3233
"internal": [
3334
"./dist/exports/internal.d.ts"
3435
],
36+
"react": [
37+
"./dist/exports/react.d.ts"
38+
],
3539
"postgres": [
3640
"./dist/postgres/index.d.ts"
3741
],
@@ -95,16 +99,39 @@
9599
"zustand": "^4.3.7"
96100
},
97101
"devDependencies": {
102+
"@tanstack/react-query": "^5.56.2",
103+
"@testing-library/react": "^16.0.0",
104+
"@testing-library/react-hooks": "^8.0.1",
98105
"@types/debug": "^4.1.7",
99106
"@types/node": "20.12.12",
107+
"@types/react": "18.2.22",
100108
"@types/sql.js": "^1.4.4",
101109
"@viem/anvil": "^0.0.7",
110+
"eslint-plugin-react": "7.31.11",
111+
"eslint-plugin-react-hooks": "4.6.0",
112+
"react": "18.2.0",
113+
"react-dom": "18.2.0",
102114
"tsup": "^6.7.0",
103115
"viem": "2.21.19",
104-
"vitest": "0.34.6"
116+
"vitest": "0.34.6",
117+
"wagmi": "2.12.11"
105118
},
106119
"peerDependencies": {
107-
"viem": "2.x"
120+
"@tanstack/react-query": "5.x",
121+
"react": "18.x",
122+
"viem": "2.x",
123+
"wagmi": "2.x"
124+
},
125+
"peerDependenciesMeta": {
126+
"@tanstack/react-query": {
127+
"optional": true
128+
},
129+
"react": {
130+
"optional": true
131+
},
132+
"wagmi": {
133+
"optional": true
134+
}
108135
},
109136
"publishConfig": {
110137
"access": "public"

packages/store-sync/src/common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ export type SyncResult = {
126126
waitForTransaction: (tx: Hex) => Promise<WaitForTransactionResult>;
127127
};
128128

129+
export type SyncAdapter = (opts: SyncOptions) => Promise<SyncResult>;
130+
129131
// TODO: add optional, original log to this?
130132
export type StorageAdapterLog = Partial<StoreEventsLog> & UnionPick<StoreEventsLog, "address" | "eventName" | "args">;
131133
export type StorageAdapterBlock = { blockNumber: BlockLogs["blockNumber"]; logs: readonly StorageAdapterLog[] };
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
1+
// SQL
12
export * from "../sql";
2-
export * from "../stash";
3+
4+
// Stash
5+
export * from "../stash/common";
6+
export * from "../stash/createStorageAdapter";
7+
export * from "../stash/createSyncAdapter";
8+
export * from "../stash/syncToStash";
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "../react/SyncProvider";
2+
export * from "../react/useSync";
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { ReactNode, createContext, useContext, useEffect } from "react";
2+
import { useConfig } from "wagmi";
3+
import { getClient } from "wagmi/actions";
4+
import { useQuery } from "@tanstack/react-query";
5+
import { SyncAdapter, SyncOptions, SyncResult } from "../common";
6+
7+
/** @internal */
8+
export const SyncContext = createContext<{
9+
sync?: SyncResult;
10+
} | null>(null);
11+
12+
export type Props = Omit<SyncOptions, "publicClient"> & {
13+
chainId: number;
14+
adapter: SyncAdapter;
15+
children: ReactNode;
16+
};
17+
18+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
19+
export function SyncProvider({ chainId, adapter, children, ...syncOptions }: Props) {
20+
const existingValue = useContext(SyncContext);
21+
if (existingValue != null) {
22+
throw new Error("A `SyncProvider` cannot be nested inside another.");
23+
}
24+
25+
const config = useConfig();
26+
27+
const { data: sync, error: syncError } = useQuery({
28+
queryKey: ["sync", chainId],
29+
queryFn: async () => {
30+
const client = getClient(config, { chainId });
31+
if (!client) {
32+
throw new Error(`Unable to retrieve Viem client for chain ${chainId}.`);
33+
}
34+
35+
return adapter({ publicClient: client, ...syncOptions });
36+
},
37+
staleTime: Infinity,
38+
refetchOnMount: false,
39+
refetchOnWindowFocus: false,
40+
refetchOnReconnect: false,
41+
});
42+
if (syncError) throw syncError;
43+
44+
useEffect(() => {
45+
if (!sync) return;
46+
47+
const sub = sync.storedBlockLogs$.subscribe({
48+
error: (error) => console.error("got sync error", error),
49+
});
50+
51+
return (): void => {
52+
sub.unsubscribe();
53+
};
54+
}, [sync]);
55+
56+
return <SyncContext.Provider value={{ sync }}>{children}</SyncContext.Provider>;
57+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useContext } from "react";
2+
import { SyncContext } from "./SyncProvider";
3+
import { SyncResult } from "../common";
4+
5+
export function useSync(): Partial<SyncResult> {
6+
const value = useContext(SyncContext);
7+
if (value == null) {
8+
throw new Error("`useSync` must be used inside a `SyncProvider`.");
9+
}
10+
const { sync } = value;
11+
return sync ?? {};
12+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { defineTable } from "@latticexyz/store/internal";
2+
import { SyncStep } from "../SyncStep";
3+
import { getSchemaPrimitives, getValueSchema } from "@latticexyz/protocol-parser/internal";
4+
5+
export const SyncProgress = defineTable({
6+
namespaceLabel: "syncToStash",
7+
label: "SyncProgress",
8+
schema: {
9+
step: "string",
10+
percentage: "uint32",
11+
latestBlockNumber: "uint256",
12+
lastBlockNumberProcessed: "uint256",
13+
message: "string",
14+
},
15+
key: [],
16+
});
17+
18+
export const initialProgress = {
19+
step: SyncStep.INITIALIZE,
20+
percentage: 0,
21+
latestBlockNumber: 0n,
22+
lastBlockNumberProcessed: 0n,
23+
message: "Connecting",
24+
} satisfies getSchemaPrimitives<getValueSchema<typeof SyncProgress>>;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { getRecord, setRecord, registerTable, Stash } from "@latticexyz/stash/internal";
2+
import { createStorageAdapter } from "./createStorageAdapter";
3+
import { SyncStep } from "../SyncStep";
4+
import { SyncAdapter } from "../common";
5+
import { createStoreSync } from "../createStoreSync";
6+
import { SyncProgress } from "./common";
7+
8+
export type CreateSyncAdapterOptions = { stash: Stash };
9+
10+
export function createSyncAdapter({ stash }: CreateSyncAdapterOptions): SyncAdapter {
11+
return (opts) => {
12+
// TODO: clear stash?
13+
14+
registerTable({ stash, table: SyncProgress });
15+
16+
const storageAdapter = createStorageAdapter({ stash });
17+
18+
return createStoreSync({
19+
...opts,
20+
storageAdapter,
21+
onProgress: (nextValue) => {
22+
const currentValue = getRecord({ stash, table: SyncProgress, key: {} });
23+
// update sync progress until we're caught up and live
24+
if (currentValue?.step !== SyncStep.LIVE) {
25+
setRecord({ stash, table: SyncProgress, key: {}, value: nextValue });
26+
}
27+
},
28+
});
29+
};
30+
}

packages/store-sync/src/stash/index.ts

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)