Skip to content

feat: Suspense support #237

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ This library consists of 6 modules with many hooks:

All hooks can be imported from `react-firehooks` directly or via `react-firehooks/<module>` to improve tree-shaking and bundle size.

All hooks suffixed with `Once` can be used in [React suspense-mode](docs/react-suspense.md).

## Development

### Build
Expand Down
5 changes: 4 additions & 1 deletion docs/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ Returns:
Returns the DataSnapshot of the Realtime Database query. Does not update the DataSnapshot once initially fetched

```javascript
const [dataSnap, loading, error] = useObjectOnce(query);
const [dataSnap, loading, error] = useObjectOnce(query, options);
```

Params:

- `query`: Realtime Database query
- `options`: Options to configure how the object is fetched
- `suspense`: Whether to use React suspense-mode. Default: `false`. [Read more](docs/react-suspense.md)

Returns:

Expand Down Expand Up @@ -73,6 +75,7 @@ Params:
- `query`: Realtime Database query
- `options`: Options to configure how the object is fetched
- `converter`: Function to extract the desired data from the DataSnapshot. Similar to Firestore converters. Default: `snap.val()`.
- `suspense`: Whether to use React suspense-mode. Default: `false`. [Read more](docs/react-suspense.md)

Returns:

Expand Down
16 changes: 14 additions & 2 deletions docs/firestore.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import { ... } from 'react-firehooks/firestore';
Returns the number of documents in the result set of of a Firestore Query. Does not update the count once initially calculated.

```javascript
const [count, loading, error] = useCountFromServer(query);
const [count, loading, error] = useCountFromServer(query, options);
```

Params:

- `query`: Firestore query whose result set size is calculated
- `options`: Options to configure how the number of documents is fetched
- `suspense`: Whether to use React suspense-mode. Default: `false`. [Read more](docs/react-suspense.md)

Returns:

Expand Down Expand Up @@ -72,6 +74,9 @@ Params:

- `documentReference`: Firestore DocumentReference that will be fetched
- `options`: Options to configure the document will be fetched
- `source`: Firestore source to fetch the document from. Default: `default`. [Read more](https://firebase.google.com/docs/firestore/query-data/get-data#source_options)
- `snapshotOptions`: Options to configure the snapshot. [Read more](https://firebase.google.com/docs/reference/js/firestore_.snapshotoptions)
- `suspense`: Whether to use React suspense-mode. Default: `false`. [Read more](docs/react-suspense.md)

Returns:

Expand All @@ -90,7 +95,9 @@ const [querySnap, loading, error] = useDocumentData(documentReference, options);
Params:

- `documentReference`: Firestore DocumentReference that will be fetched
- `options`: Options to configure how the document will be fetched
- `options`: Options to configure the document will be fetched
- `source`: Firestore source to fetch the document from. Default: `default`. [Read more](https://firebase.google.com/docs/firestore/query-data/get-data#source_options)
- `suspense`: Whether to use React suspense-mode. Default: `false`. [Read more](docs/react-suspense.md)

Returns:

Expand Down Expand Up @@ -188,6 +195,9 @@ Params:

- `query`: Firestore query that will be fetched
- `options`: Options to configure how the query is fetched
- `source`: Firestore source to fetch the document from. Default: `default`. [Read more](https://firebase.google.com/docs/firestore/query-data/get-data#source_options)
- `snapshotOptions`: Options to configure the snapshot. [Read more](https://firebase.google.com/docs/reference/js/firestore_.snapshotoptions)
- `suspense`: Whether to use React suspense-mode. Default: `false`. [Read more](docs/react-suspense.md)

Returns:

Expand All @@ -207,6 +217,8 @@ Params:

- `query`: Firestore query that will be fetched
- `options`: Options to configure how the query is fetched
- `source`: Firestore source to fetch the document from. Default: `default`. [Read more](https://firebase.google.com/docs/firestore/query-data/get-data#source_options)
- `suspense`: Whether to use React suspense-mode. Default: `false`. [Read more](docs/react-suspense.md)

Returns:

Expand Down
2 changes: 2 additions & 0 deletions docs/message.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Params:

- `messaging`: Firestore Messaging instance
- `options`: Options to configure how the token will be fetched
- `getTokenOptions`: Options to configure how the token will be fetched. [Read more](https://firebase.google.com/docs/reference/js/messaging_.gettokenoptions)
- `suspense`: Whether to use React suspense-mode. Default: `false`. [Read more](docs/react-suspense.md)

Returns:

Expand Down
18 changes: 18 additions & 0 deletions docs/react-suspense.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# React Suspense

Hooks suffixed with `Once` can be used in React `suspense`-mode by passing `suspense: true` in the options object. When using suspense-mode, the component must be wrapped in a `<Suspense>`. The second (`loading`) and third (`error`) item in the returned tuple are static and cannot be used for loading state or error handling. Errors must be handled by a wrapping [error boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary).

```jsx
function App() {
return (
<Suspense fallback={<>Loading...</>}>
<MyComponent />
</Suspense>
);
}

function MyComponent() {
const [todos] = useQueryDataOnce(collection("todos", firestore), { suspense: true });
return <>{JSON.stringify(todos)}</>;
}
```
16 changes: 13 additions & 3 deletions docs/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ const [data, loading, error] = useBlob(storageReference);
Params:

- `reference`: Reference to a Google Cloud Storage object
- `maxDownloadSizeBytes`: If set, the maximum allowed size in bytes to retrieve.
- `options`: Options to configure how the object is fetched
- `maxDownloadSizeBytes`: If set, the maximum allowed size in bytes to retrieve.
- `suspense`: Whether to use React suspense-mode. Default: `false`. [Read more](docs/react-suspense.md)

Returns:

Expand All @@ -36,7 +38,9 @@ const [data, loading, error] = useBytes(storageReference);
Params:

- `reference`: Reference to a Google Cloud Storage object
- `maxDownloadSizeBytes`: If set, the maximum allowed size in bytes to retrieve.
- `options`: Options to configure how the object is fetched
- `maxDownloadSizeBytes`: If set, the maximum allowed size in bytes to retrieve.
- `suspense`: Whether to use React suspense-mode. Default: `false`. [Read more](docs/react-suspense.md)

Returns:

Expand All @@ -55,6 +59,8 @@ const [url, loading, error] = useDownloadURL(storageReference);
Params:

- `reference`: Reference to a Google Cloud Storage object
- `options`: Options to configure how the download URL is fetched
- `suspense`: Whether to use React suspense-mode. Default: `false`. [Read more](docs/react-suspense.md)

Returns:

Expand All @@ -73,6 +79,8 @@ const [metadata, loading, error] = useMetadata(storageReference);
Params:

- `reference`: Reference to a Google Cloud Storage object
- `options`: Options to configure how the metadata is fetched
- `suspense`: Whether to use React suspense-mode. Default: `false`. [Read more](docs/react-suspense.md)

Returns:

Expand All @@ -93,7 +101,9 @@ const [data, loading, error] = useStream(storageReference);
Params:

- `reference`: Reference to a Google Cloud Storage object
- `maxDownloadSizeBytes`: If set, the maximum allowed size in bytes to retrieve.
- `options`: Options to configure how the object is fetched
- `maxDownloadSizeBytes`: If set, the maximum allowed size in bytes to retrieve.
- `suspense`: Whether to use React suspense-mode. Default: `false`. [Read more](docs/react-suspense.md)

Returns:

Expand Down
16 changes: 14 additions & 2 deletions src/database/useObjectOnce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,27 @@ import { isQueryEqual } from "./internal.js";

export type UseObjectOnceResult = ValueHookResult<DataSnapshot, Error>;

/**
* Options to configure how the object is fetched
*/
export interface UseObjectOnceOptions {
/**
* @default false
*/
suspense?: boolean;
}

/**
* Returns and updates the DataSnapshot of the Realtime Database query. Does not update the DataSnapshot once initially fetched
* @param query Realtime Database query
* @param [options] Options to configure how the object is fetched
* @returns User, loading state, and error
* value: DataSnapshot; `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 useObjectOnce(query: Query | undefined | null): UseObjectOnceResult {
export function useObjectOnce(query: Query | undefined | null, options?: UseObjectOnceOptions): UseObjectOnceResult {
const { suspense = false } = options ?? {};
const getData = useCallback((stableQuery: Query) => get(stableQuery), []);
return useOnce(query ?? undefined, getData, isQueryEqual);
return useOnce(query ?? undefined, getData, isQueryEqual, suspense);
}
18 changes: 14 additions & 4 deletions src/database/useObjectValueOnce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,26 @@ export type UseObjectValueOnceResult<Value = unknown> = ValueHookResult<Value, E

export type UseObjectValueOnceConverter<Value> = (snap: DataSnapshot) => Value;

/**
* Options to configure how the object is fetched
*/
export interface UseObjectValueOnceOptions<Value> {
/**
* Function to extract the desired data from the DataSnapshot. Similar to Firestore converters. Default: `snap.val()`.
*/
converter?: UseObjectValueOnceConverter<Value>;

/**
* @default false
*/
suspense?: boolean;
}

/**
* Returns the DataSnapshot of the Realtime Database query. Does not update the DataSnapshot once initially fetched
* @template Value Type of the object value
* @param query Realtime Database query
* @param options Options to configure how the object is fetched
* `converter`: Function to extract the desired data from the DataSnapshot. Similar to Firestore converters. Default: `snap.val()`.
* @param [options] Options to configure how the object is fetched
* @returns User, loading state, and error
* value: Object value; `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
Expand All @@ -27,7 +37,7 @@ export function useObjectValueOnce<Value = unknown>(
query: Query | undefined | null,
options?: UseObjectValueOnceOptions<Value>,
): UseObjectValueOnceResult<Value> {
const { converter = (snap: DataSnapshot) => snap.val() } = options ?? {};
const { converter = (snap: DataSnapshot) => snap.val(), suspense = false } = options ?? {};

const getData = useCallback(async (stableQuery: Query) => {
const snap = await get(stableQuery);
Expand All @@ -36,5 +46,5 @@ export function useObjectValueOnce<Value = unknown>(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return useOnce(query ?? undefined, getData, isQueryEqual);
return useOnce(query ?? undefined, getData, isQueryEqual, suspense);
}
19 changes: 17 additions & 2 deletions src/firestore/useCountFromServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ import { isQueryEqual } from "./internal.js";

export type UseCountFromServerResult = ValueHookResult<number, FirestoreError>;

/**
* Options to configure how the number of documents is fetched
*/
export interface UseCountFromServerOptions {
/**
* @default false
*/
suspense?: boolean;
}

// eslint-disable-next-line jsdoc/require-param, jsdoc/require-returns
/**
* @internal
Expand All @@ -17,11 +27,16 @@ async function getData(stableQuery: Query<unknown>): Promise<number> {
/**
* Returns the number of documents in the result set of a Firestore Query. Does not update the count once initially calculated.
* @param query Firestore query whose result set size is calculated
* @param [options] Options to configure how the number of documents is fetched
* @returns Size of the result set, loading state, and error
* value: Size of the result set; `undefined` if the result set size is currently being calculated, or an error occurred
* loading: `true` while calculating the result size set; `false` if the result size set was calculated successfully or an error occurred
* error: `undefined` if no error occurred
*/
export function useCountFromServer(query: Query<unknown> | undefined | null): UseCountFromServerResult {
return useOnce(query ?? undefined, getData, isQueryEqual);
export function useCountFromServer(
query: Query<unknown> | undefined | null,
options?: UseCountFromServerOptions,
): UseCountFromServerResult {
const { suspense = false } = options ?? {};
return useOnce(query ?? undefined, getData, isQueryEqual, suspense);
}
19 changes: 14 additions & 5 deletions src/firestore/useDocumentDataOnce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,40 @@ export type UseDocumentDataOnceResult<Value extends DocumentData = DocumentData>
* Options to configure how the document is fetched
*/
export interface UseDocumentDataOnceOptions {
/**
* @default "default"
*/
source?: Source;

snapshotOptions?: SnapshotOptions;

/**
* @default false
*/
suspense?: boolean;
}

/**
* Returns the data of a Firestore DocumentReference
* @template Value Type of the document data
* @param reference Firestore DocumentReference that will be subscribed to
* @param options Options to configure how the document is fetched
* @param [options] Options to configure how the document is fetched
* @returns Document data, loading state, and error
* value: Document data; `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
* loading: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred; Always `false` with `supsense=true`
* error: `undefined` if no error occurred; Always `undefined` with `supsense=true`
*/
export function useDocumentDataOnce<Value extends DocumentData = DocumentData>(
reference: DocumentReference<Value> | undefined | null,
options?: UseDocumentDataOnceOptions,
): UseDocumentDataOnceResult<Value> {
const { source = "default", snapshotOptions } = options ?? {};
const { source = "default", snapshotOptions, suspense = false } = options ?? {};

const getData = useCallback(async (stableRef: DocumentReference<Value>) => {
const snap = await getDocFromSource(stableRef, source);
return snap.data(snapshotOptions);
// TODO: add options as dependency
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return useOnce(reference ?? undefined, getData, isDocRefEqual);
return useOnce(reference ?? undefined, getData, isDocRefEqual, suspense);
}
18 changes: 13 additions & 5 deletions src/firestore/useDocumentOnce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,38 @@ export type UseDocumentOnceResult<Value extends DocumentData = DocumentData> = V
* Options to configure how the document is fetched
*/
export interface UseDocumentOnceOptions {
/**
* @default "default"
*/
source?: Source;

/**
* @default false
*/
suspense?: boolean;
}

/**
* Returns the DocumentSnapshot of a Firestore DocumentReference. Does not update the DocumentSnapshot once initially fetched
* @template Value Type of the document data
* @param reference Firestore DocumentReference that will be fetched
* @param options Options to configure how the document is fetched
* @param [options] Options to configure how the document is fetched
* @returns DocumentSnapshot, loading state, and error
* 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
* loading: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred; Always `false` with `supsense=true`
* error: `undefined` if no error occurred; Always `undefined` with `supsense=true`
*/
export function useDocumentOnce<Value extends DocumentData = DocumentData>(
reference: DocumentReference<Value> | undefined | null,
options?: UseDocumentOnceOptions,
): UseDocumentOnceResult<Value> {
const { source = "default" } = options ?? {};
const { source = "default", suspense = false } = options ?? {};

const getData = useCallback(
(stableRef: DocumentReference<Value>) => getDocFromSource(stableRef, source),
// TODO: add options as dependency
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
return useOnce(reference ?? undefined, getData, isDocRefEqual);
return useOnce(reference ?? undefined, getData, isDocRefEqual, suspense);
}
Loading