Skip to content

Commit

Permalink
feat: support React suspense (#229)
Browse files Browse the repository at this point in the history
  • Loading branch information
andipaetzold committed Sep 12, 2023
1 parent cf0277e commit afda4e4
Show file tree
Hide file tree
Showing 27 changed files with 723 additions and 202 deletions.
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);
}

0 comments on commit afda4e4

Please sign in to comment.