Skip to content

Commit afda4e4

Browse files
authored
feat: support React suspense (#229)
1 parent cf0277e commit afda4e4

27 files changed

+723
-202
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ This library consists of 6 modules with many hooks:
4141

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

44+
All hooks suffixed with `Once` can be used in [React suspense-mode](docs/react-suspense.md).
45+
4446
## Development
4547

4648
### Build

docs/database.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ Returns:
2727
Returns the DataSnapshot of the Realtime Database query. Does not update the DataSnapshot once initially fetched
2828

2929
```javascript
30-
const [dataSnap, loading, error] = useObjectOnce(query);
30+
const [dataSnap, loading, error] = useObjectOnce(query, options);
3131
```
3232

3333
Params:
3434

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

3739
Returns:
3840

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

7780
Returns:
7881

docs/firestore.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import { ... } from 'react-firehooks/firestore';
99
Returns the number of documents in the result set of of a Firestore Query. Does not update the count once initially calculated.
1010

1111
```javascript
12-
const [count, loading, error] = useCountFromServer(query);
12+
const [count, loading, error] = useCountFromServer(query, options);
1313
```
1414

1515
Params:
1616

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

1921
Returns:
2022

@@ -72,6 +74,9 @@ Params:
7274

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

7681
Returns:
7782

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

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

95102
Returns:
96103

@@ -188,6 +195,9 @@ Params:
188195

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

192202
Returns:
193203

@@ -207,6 +217,8 @@ Params:
207217

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

211223
Returns:
212224

docs/message.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Params:
1616

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

2022
Returns:
2123

docs/react-suspense.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# React Suspense
2+
3+
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).
4+
5+
```jsx
6+
function App() {
7+
return (
8+
<Suspense fallback={<>Loading...</>}>
9+
<MyComponent />
10+
</Suspense>
11+
);
12+
}
13+
14+
function MyComponent() {
15+
const [todos] = useQueryDataOnce(collection("todos", firestore), { suspense: true });
16+
return <>{JSON.stringify(todos)}</>;
17+
}
18+
```

docs/storage.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ const [data, loading, error] = useBlob(storageReference);
1717
Params:
1818

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

2224
Returns:
2325

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

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

4145
Returns:
4246

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

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

5965
Returns:
6066

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

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

7785
Returns:
7886

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

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

98108
Returns:
99109

src/database/useObjectOnce.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,27 @@ import { isQueryEqual } from "./internal.js";
66

77
export type UseObjectOnceResult = ValueHookResult<DataSnapshot, Error>;
88

9+
/**
10+
* Options to configure how the object is fetched
11+
*/
12+
export interface UseObjectOnceOptions {
13+
/**
14+
* @default false
15+
*/
16+
suspense?: boolean;
17+
}
18+
919
/**
1020
* Returns and updates the DataSnapshot of the Realtime Database query. Does not update the DataSnapshot once initially fetched
1121
* @param query Realtime Database query
22+
* @param [options] Options to configure how the object is fetched
1223
* @returns User, loading state, and error
1324
* value: DataSnapshot; `undefined` if query is currently being fetched, or an error occurred
1425
* loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred
1526
* error: `undefined` if no error occurred
1627
*/
17-
export function useObjectOnce(query: Query | undefined | null): UseObjectOnceResult {
28+
export function useObjectOnce(query: Query | undefined | null, options?: UseObjectOnceOptions): UseObjectOnceResult {
29+
const { suspense = false } = options ?? {};
1830
const getData = useCallback((stableQuery: Query) => get(stableQuery), []);
19-
return useOnce(query ?? undefined, getData, isQueryEqual);
31+
return useOnce(query ?? undefined, getData, isQueryEqual, suspense);
2032
}

src/database/useObjectValueOnce.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,26 @@ export type UseObjectValueOnceResult<Value = unknown> = ValueHookResult<Value, E
88

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

11+
/**
12+
* Options to configure how the object is fetched
13+
*/
1114
export interface UseObjectValueOnceOptions<Value> {
15+
/**
16+
* Function to extract the desired data from the DataSnapshot. Similar to Firestore converters. Default: `snap.val()`.
17+
*/
1218
converter?: UseObjectValueOnceConverter<Value>;
19+
20+
/**
21+
* @default false
22+
*/
23+
suspense?: boolean;
1324
}
1425

1526
/**
1627
* Returns the DataSnapshot of the Realtime Database query. Does not update the DataSnapshot once initially fetched
1728
* @template Value Type of the object value
1829
* @param query Realtime Database query
19-
* @param options Options to configure how the object is fetched
20-
* `converter`: Function to extract the desired data from the DataSnapshot. Similar to Firestore converters. Default: `snap.val()`.
30+
* @param [options] Options to configure how the object is fetched
2131
* @returns User, loading state, and error
2232
* value: Object value; `undefined` if query is currently being fetched, or an error occurred
2333
* loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred
@@ -27,7 +37,7 @@ export function useObjectValueOnce<Value = unknown>(
2737
query: Query | undefined | null,
2838
options?: UseObjectValueOnceOptions<Value>,
2939
): UseObjectValueOnceResult<Value> {
30-
const { converter = (snap: DataSnapshot) => snap.val() } = options ?? {};
40+
const { converter = (snap: DataSnapshot) => snap.val(), suspense = false } = options ?? {};
3141

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

39-
return useOnce(query ?? undefined, getData, isQueryEqual);
49+
return useOnce(query ?? undefined, getData, isQueryEqual, suspense);
4050
}

src/firestore/useCountFromServer.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ import { isQueryEqual } from "./internal.js";
55

66
export type UseCountFromServerResult = ValueHookResult<number, FirestoreError>;
77

8+
/**
9+
* Options to configure how the number of documents is fetched
10+
*/
11+
export interface UseCountFromServerOptions {
12+
/**
13+
* @default false
14+
*/
15+
suspense?: boolean;
16+
}
17+
818
// eslint-disable-next-line jsdoc/require-param, jsdoc/require-returns
919
/**
1020
* @internal
@@ -17,11 +27,16 @@ async function getData(stableQuery: Query<unknown>): Promise<number> {
1727
/**
1828
* Returns the number of documents in the result set of a Firestore Query. Does not update the count once initially calculated.
1929
* @param query Firestore query whose result set size is calculated
30+
* @param [options] Options to configure how the number of documents is fetched
2031
* @returns Size of the result set, loading state, and error
2132
* value: Size of the result set; `undefined` if the result set size is currently being calculated, or an error occurred
2233
* loading: `true` while calculating the result size set; `false` if the result size set was calculated successfully or an error occurred
2334
* error: `undefined` if no error occurred
2435
*/
25-
export function useCountFromServer(query: Query<unknown> | undefined | null): UseCountFromServerResult {
26-
return useOnce(query ?? undefined, getData, isQueryEqual);
36+
export function useCountFromServer(
37+
query: Query<unknown> | undefined | null,
38+
options?: UseCountFromServerOptions,
39+
): UseCountFromServerResult {
40+
const { suspense = false } = options ?? {};
41+
return useOnce(query ?? undefined, getData, isQueryEqual, suspense);
2742
}

src/firestore/useDocumentDataOnce.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,40 @@ export type UseDocumentDataOnceResult<Value extends DocumentData = DocumentData>
1111
* Options to configure how the document is fetched
1212
*/
1313
export interface UseDocumentDataOnceOptions {
14+
/**
15+
* @default "default"
16+
*/
1417
source?: Source;
18+
1519
snapshotOptions?: SnapshotOptions;
20+
21+
/**
22+
* @default false
23+
*/
24+
suspense?: boolean;
1625
}
1726

1827
/**
1928
* Returns the data of a Firestore DocumentReference
2029
* @template Value Type of the document data
2130
* @param reference Firestore DocumentReference that will be subscribed to
22-
* @param options Options to configure how the document is fetched
31+
* @param [options] Options to configure how the document is fetched
2332
* @returns Document data, loading state, and error
2433
* value: Document data; `undefined` if document does not exist, is currently being fetched, or an error occurred
25-
* loading: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred
26-
* error: `undefined` if no error occurred
34+
* loading: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred; Always `false` with `supsense=true`
35+
* error: `undefined` if no error occurred; Always `undefined` with `supsense=true`
2736
*/
2837
export function useDocumentDataOnce<Value extends DocumentData = DocumentData>(
2938
reference: DocumentReference<Value> | undefined | null,
3039
options?: UseDocumentDataOnceOptions,
3140
): UseDocumentDataOnceResult<Value> {
32-
const { source = "default", snapshotOptions } = options ?? {};
41+
const { source = "default", snapshotOptions, suspense = false } = options ?? {};
3342

3443
const getData = useCallback(async (stableRef: DocumentReference<Value>) => {
3544
const snap = await getDocFromSource(stableRef, source);
3645
return snap.data(snapshotOptions);
3746
// TODO: add options as dependency
3847
// eslint-disable-next-line react-hooks/exhaustive-deps
3948
}, []);
40-
return useOnce(reference ?? undefined, getData, isDocRefEqual);
49+
return useOnce(reference ?? undefined, getData, isDocRefEqual, suspense);
4150
}

src/firestore/useDocumentOnce.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,38 @@ export type UseDocumentOnceResult<Value extends DocumentData = DocumentData> = V
1414
* Options to configure how the document is fetched
1515
*/
1616
export interface UseDocumentOnceOptions {
17+
/**
18+
* @default "default"
19+
*/
1720
source?: Source;
21+
22+
/**
23+
* @default false
24+
*/
25+
suspense?: boolean;
1826
}
1927

2028
/**
2129
* Returns the DocumentSnapshot of a Firestore DocumentReference. Does not update the DocumentSnapshot once initially fetched
2230
* @template Value Type of the document data
2331
* @param reference Firestore DocumentReference that will be fetched
24-
* @param options Options to configure how the document is fetched
32+
* @param [options] Options to configure how the document is fetched
2533
* @returns DocumentSnapshot, loading state, and error
2634
* value: DocumentSnapshot; `undefined` if document does not exist, is currently being fetched, or an error occurred
27-
* loading: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred
28-
* error: `undefined` if no error occurred
35+
* loading: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred; Always `false` with `supsense=true`
36+
* error: `undefined` if no error occurred; Always `undefined` with `supsense=true`
2937
*/
3038
export function useDocumentOnce<Value extends DocumentData = DocumentData>(
3139
reference: DocumentReference<Value> | undefined | null,
3240
options?: UseDocumentOnceOptions,
3341
): UseDocumentOnceResult<Value> {
34-
const { source = "default" } = options ?? {};
42+
const { source = "default", suspense = false } = options ?? {};
3543

3644
const getData = useCallback(
3745
(stableRef: DocumentReference<Value>) => getDocFromSource(stableRef, source),
3846
// TODO: add options as dependency
3947
// eslint-disable-next-line react-hooks/exhaustive-deps
4048
[],
4149
);
42-
return useOnce(reference ?? undefined, getData, isDocRefEqual);
50+
return useOnce(reference ?? undefined, getData, isDocRefEqual, suspense);
4351
}

0 commit comments

Comments
 (0)