Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fix-react-use-query-sdk-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"zerithdb-react": patch
---

Fix `useQuery` to call the public SDK `app.db(collectionName)` API, load collection data without
relying on an unavailable `subscribe` method, and refresh query state after insert/delete mutations.
79 changes: 55 additions & 24 deletions packages/react/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { createContext, useContext, useEffect, useState, useMemo } from "react";
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { createApp } from "zerithdb-sdk";
import type { ZerithDBConfig } from "zerithdb-sdk";
import type { Document, QueryFilter, ZerithDBApp, ZerithDBConfig } from "zerithdb-sdk";

const ZerithContext = createContext<any>(null);
const ZerithContext = createContext<ZerithDBApp | null>(null);

export interface ZerithProviderProps {
config: ZerithDBConfig;
Expand All @@ -22,7 +22,7 @@ export const ZerithProvider: React.FC<ZerithProviderProps> = ({ config, children
/**
* Access the underlying ZerithDB client directly.
*/
export const useZerith = () => {
export const useZerith = (): ZerithDBApp => {
const context = useContext(ZerithContext);
if (!context) {
throw new Error("useZerith must be used within a ZerithProvider");
Expand All @@ -31,41 +31,72 @@ export const useZerith = () => {
};

/**
* Reactive hook to query a collection.
* Automatically updates when local or remote (P2P) changes occur.
* Hook to query and mutate a collection.
* Refreshes local state after mutations.
*/
export function useQuery<T = any>(collectionName: string) {
const db = useZerith() as any;
const [data, setData] = useState<T[]>([]);
export function useQuery<T extends Record<string, any> = Record<string, any>>(
collectionName: string
) {
const app = useZerith();
const [data, setData] = useState<Document<T>[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

const refresh = useCallback(async (): Promise<void> => {
setLoading(true);
setError(null);

try {
const docs = await app.db<T>(collectionName).find({});
setData(docs);
} catch (err) {
setError(err instanceof Error ? err : new Error("Failed to query collection"));
} finally {
setLoading(false);
}
}, [app, collectionName]);

useEffect(() => {
let mounted = true;
let cancelled = false;

const collection = db.collection(collectionName);
const load = async (): Promise<void> => {
setLoading(true);
setError(null);

// Subscribe to real-time updates (CRDT merges)
const unsubscribe = collection.subscribe((docs: any[]) => {
if (mounted) {
setData(docs as T[]);
setLoading(false);
try {
const docs = await app.db<T>(collectionName).find({});
if (!cancelled) {
setData(docs);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err : new Error("Failed to query collection"));
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
});
};

void load();

return () => {
mounted = false;
unsubscribe();
cancelled = true;
};
}, [db, collectionName]);
}, [app, collectionName]);

const insert = async (item: Partial<T>) => {
return db.collection(collectionName).insert(item);
const insert = async (item: T) => {
const result = await app.db<T>(collectionName).insert(item);
await refresh();
return result;
};

const remove = async (id: string) => {
return db.collection(collectionName).delete(id);
const count = await app.db<T>(collectionName).delete({ _id: id } as unknown as QueryFilter<T>);
await refresh();
return count;
};

return { data, loading, error, insert, remove };
return { data, loading, error, refresh, insert, remove };
}