Skip to content

Commit

Permalink
feat: useQueries & useQueriesData (#193)
Browse files Browse the repository at this point in the history
  • Loading branch information
andipaetzold committed Feb 11, 2023
1 parent 0361354 commit 03be7c8
Show file tree
Hide file tree
Showing 13 changed files with 705 additions and 12 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ jobs:
run: npm run build
- name: Test
run: npm test -- --run --coverage
- name: Typecheck
run: npm run typecheck -- --run

release:
name: Release
Expand Down
40 changes: 40 additions & 0 deletions docs/firestore.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,43 @@ Returns:
- `value`: DocumentSnapshot; `undefined` if document does not exist, is currently being fetched, or an error occurred
- `loading`: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred
- `error`: `undefined` if no error occurred

## useQueries

Returns and updates a QuerySnapshot of multiple Firestore queries

```javascript
const [querySnap, loading, error] = useQueries(queries, options);
```

Params:

- `queries`: Firestore queries that will be subscribed to
- `options`: Options to configure the subscription

Returns:

- Array with tuple for each query:
- `value`: QuerySnapshot; `undefined` if query is currently being fetched, or an error occurred
- `loading`: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred
- `error`: `undefined` if no error occurred

## useQueriesData

Returns and updates a the document data of multiple Firestore queries

```javascript
const [querySnap, loading, error] = useQueriesData(query, options);
```

Params:

- `queries`: Firestore queries that will be subscribed to
- `options`: Options to configure the subscription

Returns:

- Array with tuple for each query:
- `value`: Query data; `undefined` if query is currently being fetched, or an error occurred
- `loading` :`true` while fetching the query; `false` if the query was fetched successfully or an error occurred
- `error`: `undefined` if no error occurred
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"build:esm": "rimraf lib && tsc",
"build:modules": "node ./scripts/create-modules.js",
"test": "vitest",
"typecheck": "vitest typecheck",
"semantic-release": "semantic-release",
"typedoc": "typedoc",
"prepare": "husky install",
Expand Down
2 changes: 2 additions & 0 deletions src/firestore/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export * from "./useDocument.js";
export * from "./useDocumentData.js";
export * from "./useDocumentDataOnce.js";
export * from "./useDocumentOnce.js";
export * from "./useQueries.js";
export * from "./useQueriesData.js";
export * from "./useQuery.js";
export * from "./useQueryData.js";
export * from "./useQueryDataOnce.js";
Expand Down
26 changes: 26 additions & 0 deletions src/firestore/useQueries.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Query, QuerySnapshot } from "firebase/firestore";
import { describe, expectTypeOf, it } from "vitest";
import { useQueries } from "./useQueries";

describe("useQueries", () => {
it("single query", () => {
type Value = { key: "value" };
const query = null as unknown as Query<Value>;

const results = useQueries([query]);

expectTypeOf<typeof results[0][0]>().toMatchTypeOf<QuerySnapshot<Value> | undefined>();
});

it("multiple queries", () => {
type Value1 = { key: "value" };
type Value2 = { key: "value2" };
const query1 = null as unknown as Query<Value1>;
const query2 = null as unknown as Query<Value2>;

const results = useQueries([query1, query2] as const);

expectTypeOf<typeof results[0][0]>().toMatchTypeOf<QuerySnapshot<Value1> | undefined>();
expectTypeOf<typeof results[1][0]>().toMatchTypeOf<QuerySnapshot<Value2> | undefined>();
});
});
45 changes: 45 additions & 0 deletions src/firestore/useQueries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { DocumentData, FirestoreError, onSnapshot, Query, QuerySnapshot, SnapshotListenOptions } from "firebase/firestore";
import { useCallback } from "react";
import { ValueHookResult } from "../common/types.js";
import { useMultiListen, UseMultiListenChange } from "../internal/useMultiListen.js";
import { isQueryEqual } from "./internal.js";

export type UseQueriesResult<Values extends ReadonlyArray<DocumentData> = ReadonlyArray<DocumentData>> = {
[Index in keyof Values]: ValueHookResult<QuerySnapshot<Values[Index]>, FirestoreError>;
} & { length: Values["length"] };

/**
* Options to configure the subscription
*/
export interface UseQueriesOptions {
snapshotListenOptions?: SnapshotListenOptions;
}

/**
* Returns and updates a QuerySnapshot of multiple Firestore queries
*
* @template Values Tuple of types of the collection data
* @param {Query[]} queries Firestore queries that will be subscribed to
* @param {?UseQueriesOptions} options Options to configure the subscription
* @returns {ValueHookResult[]} Array with tuple for each query:
* * value: QuerySnapshot; `undefined` if query is currently being fetched, or an error occurred
* * loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred
* * error: `undefined` if no error occurred
*/
export function useQueries<Values extends ReadonlyArray<DocumentData> = ReadonlyArray<DocumentData>>(
queries: { [Index in keyof Values]: Query<Values[Index]> },
options?: UseQueriesOptions
): UseQueriesResult<Values> {
const { snapshotListenOptions = {} } = options ?? {};
const onChange: UseMultiListenChange<QuerySnapshot<Values[number]>, FirestoreError, Query<Values[number]>> = useCallback(
(query, next, error) =>
onSnapshot(query, snapshotListenOptions, {
next,
error,
}),
[]
);

// @ts-expect-error `useMultiListen` assumes a single value type
return useMultiListen(queries, onChange, isQueryEqual);
}
26 changes: 26 additions & 0 deletions src/firestore/useQueriesData.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Query } from "firebase/firestore";
import { describe, expectTypeOf, it } from "vitest";
import { useQueriesData } from "./useQueriesData";

describe("useQueriesData", () => {
it("single query", () => {
type Value = { key: "value" };
const query = null as unknown as Query<Value>;

const results = useQueriesData([query]);

expectTypeOf<typeof results[0][0]>().toMatchTypeOf<Value | undefined>();
});

it("multiple queries", () => {
type Value1 = { key: "value" };
type Value2 = { key: "value2" };
const query1 = null as unknown as Query<Value1>;
const query2 = null as unknown as Query<Value2>;

const results = useQueriesData([query1, query2] as const);

expectTypeOf<typeof results[0][0]>().toMatchTypeOf<Value1 | undefined>();
expectTypeOf<typeof results[1][0]>().toMatchTypeOf<Value2 | undefined>();
});
});
46 changes: 46 additions & 0 deletions src/firestore/useQueriesData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { DocumentData, FirestoreError, onSnapshot, Query, SnapshotListenOptions, SnapshotOptions } from "firebase/firestore";
import { useCallback } from "react";
import { ValueHookResult } from "../common/types.js";
import { useMultiListen, UseMultiListenChange } from "../internal/useMultiListen.js";
import { isQueryEqual } from "./internal.js";

export type UseQueriesDataResult<Values extends ReadonlyArray<DocumentData> = ReadonlyArray<DocumentData>> = {
[Index in keyof Values]: ValueHookResult<Values[Index], FirestoreError>;
} & { length: Values["length"] };

/**
* Options to configure the subscription
*/
export interface UseQueriesDataOptions {
snapshotListenOptions?: SnapshotListenOptions;
snapshotOptions?: SnapshotOptions;
}

/**
* Returns and updates a the document data of multiple Firestore queries
*
* @template Values Tuple of types of the collection data
* @param {Query[]} queries Firestore queries that will be subscribed to
* @param {?UseQueriesDataOptions} options Options to configure the subscription
* @returns {ValueHookResult[]} Array with tuple for each query:
* * value: Query data; `undefined` if query is currently being fetched, or an error occurred
* * loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred
* * error: `undefined` if no error occurred
*/
export function useQueriesData<Values extends ReadonlyArray<DocumentData> = ReadonlyArray<DocumentData>>(
queries: { [Index in keyof Values]: Query<Values[Index]> },
options?: UseQueriesDataOptions
): UseQueriesDataResult<Values> {
const { snapshotListenOptions = {}, snapshotOptions = {} } = options ?? {};
const onChange: UseMultiListenChange<Values[number], FirestoreError, Query<Values[number]>> = useCallback(
(query, next, error) =>
onSnapshot(query, snapshotListenOptions, {
next: (snap) => next(snap.docs.map((doc) => doc.data(snapshotOptions))),
error,
}),
[]
);

// @ts-expect-error `useMultiListen` assumes a single value type
return useMultiListen(queries, onChange, isQueryEqual);
}
16 changes: 4 additions & 12 deletions src/internal/useListen.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,10 @@ it("should return emitted values", () => {

expect(result.current).toStrictEqual([undefined, true, undefined]);

act(() => {
setValue(result1);
});
act(() => setValue(result1));
expect(result.current).toStrictEqual([result1, false, undefined]);

act(() => {
setValue(result2);
});
act(() => setValue(result2));
expect(result.current).toStrictEqual([result2, false, undefined]);
});

Expand All @@ -138,13 +134,9 @@ it("should return emitted error", () => {

expect(result.current).toStrictEqual([undefined, true, undefined]);

act(() => {
setError(error);
});
act(() => setError(error));
expect(result.current).toStrictEqual([undefined, false, error]);

act(() => {
setValue(result2);
});
act(() => setValue(result2));
expect(result.current).toStrictEqual([result2, false, undefined]);
});

0 comments on commit 03be7c8

Please sign in to comment.