diff --git a/docs/reference/README.md b/docs/reference/README.md index 7606dba0..f64f5ce0 100644 --- a/docs/reference/README.md +++ b/docs/reference/README.md @@ -14,7 +14,10 @@ ReactFire reference docs - [ClaimCheckErrors](interfaces/ClaimCheckErrors.md) - [ClaimsCheckProps](interfaces/ClaimsCheckProps.md) - [ClaimsValidator](interfaces/ClaimsValidator.md) -- [ObservableStatus](interfaces/ObservableStatus.md) +- [FirebaseAppProviderProps](interfaces/FirebaseAppProviderProps.md) +- [ObservableStatusError](interfaces/ObservableStatusError.md) +- [ObservableStatusLoading](interfaces/ObservableStatusLoading.md) +- [ObservableStatusSuccess](interfaces/ObservableStatusSuccess.md) - [ReactFireOptions](interfaces/ReactFireOptions.md) - [SignInCheckOptionsBasic](interfaces/SignInCheckOptionsBasic.md) - [SignInCheckOptionsClaimsObject](interfaces/SignInCheckOptionsClaimsObject.md) @@ -23,8 +26,10 @@ ReactFire reference docs ### Type Aliases +- [ObservableStatus](README.md#observablestatus) - [ReactFireGlobals](README.md#reactfireglobals) - [SigninCheckResult](README.md#signincheckresult) +- [StorageImageProps](README.md#storageimageprops) ### Variables @@ -107,6 +112,22 @@ ReactFire reference docs ## Type Aliases +### ObservableStatus + +Ƭ **ObservableStatus**<`T`\>: [`ObservableStatusLoading`](interfaces/ObservableStatusLoading.md)<`T`\> \| [`ObservableStatusError`](interfaces/ObservableStatusError.md)<`T`\> \| [`ObservableStatusSuccess`](interfaces/ObservableStatusSuccess.md)<`T`\> + +#### Type parameters + +| Name | +| :------ | +| `T` | + +#### Defined in + +[src/useObservable.ts:84](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L84) + +___ + ### ReactFireGlobals Ƭ **ReactFireGlobals**: `Object` @@ -133,6 +154,25 @@ ___ [src/auth.tsx:59](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L59) +___ + +### StorageImageProps + +Ƭ **StorageImageProps**: `Object` + +#### Type declaration + +| Name | Type | +| :------ | :------ | +| `placeHolder?` | `JSX.Element` | +| `storage?` | `FirebaseStorage` | +| `storagePath` | `string` | +| `suspense?` | `boolean` | + +#### Defined in + +[src/storage.tsx:36](https://github.com/FirebaseExtended/reactfire/blob/main/src/storage.tsx#L36) + ## Variables ### AnalyticsSdkContext @@ -299,7 +339,7 @@ Meant for Concurrent mode only (``). [More #### Defined in -[src/auth.tsx:240](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L240) +[src/auth.tsx:247](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L247) ___ @@ -379,7 +419,7 @@ ___ | Name | Type | | :------ | :------ | -| `props` | `PropsWithChildren`<`FirebaseAppProviderProps`\> | +| `props` | `PropsWithChildren`<[`FirebaseAppProviderProps`](interfaces/FirebaseAppProviderProps.md)\> | #### Returns @@ -479,7 +519,7 @@ ___ | Name | Type | | :------ | :------ | -| `props` | `StorageImageProps` & `ClassAttributes`<`HTMLImageElement`\> & `ImgHTMLAttributes`<`HTMLImageElement`\> | +| `props` | [`StorageImageProps`](README.md#storageimageprops) & `ClassAttributes`<`HTMLImageElement`\> & `ImgHTMLAttributes`<`HTMLImageElement`\> | #### Returns @@ -640,7 +680,7 @@ ___ #### Defined in -[src/useObservable.ts:19](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L19) +[src/useObservable.ts:20](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L20) ___ @@ -708,7 +748,7 @@ ___ ### useCallableFunctionResponse -▸ **useCallableFunctionResponse**<`RequestData`, `ResponseData`\>(`functionName`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`ResponseData`\> +▸ **useCallableFunctionResponse**<`RequestData`, `ResponseData`\>(`functionName`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`ResponseData`\> Calls a callable function. @@ -728,7 +768,7 @@ Calls a callable function. #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`ResponseData`\> +[`ObservableStatus`](README.md#observablestatus)<`ResponseData`\> #### Defined in @@ -752,7 +792,7 @@ ___ ### useDatabaseList -▸ **useDatabaseList**<`T`\>(`ref`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`QueryChange`[] \| `T`[]\> +▸ **useDatabaseList**<`T`\>(`ref`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`QueryChange`[] \| `T`[]\> Subscribe to a Realtime Database list @@ -771,7 +811,7 @@ Subscribe to a Realtime Database list #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`QueryChange`[] \| `T`[]\> +[`ObservableStatus`](README.md#observablestatus)<`QueryChange`[] \| `T`[]\> #### Defined in @@ -781,7 +821,7 @@ ___ ### useDatabaseListData -▸ **useDatabaseListData**<`T`\>(`ref`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`T`[] \| ``null``\> +▸ **useDatabaseListData**<`T`\>(`ref`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`T`[] \| ``null``\> #### Type parameters @@ -798,7 +838,7 @@ ___ #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`T`[] \| ``null``\> +[`ObservableStatus`](README.md#observablestatus)<`T`[] \| ``null``\> #### Defined in @@ -808,7 +848,7 @@ ___ ### useDatabaseObject -▸ **useDatabaseObject**<`T`\>(`ref`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`QueryChange` \| `T`\> +▸ **useDatabaseObject**<`T`\>(`ref`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`QueryChange` \| `T`\> Subscribe to a Realtime Database object @@ -827,7 +867,7 @@ Subscribe to a Realtime Database object #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`QueryChange` \| `T`\> +[`ObservableStatus`](README.md#observablestatus)<`QueryChange` \| `T`\> #### Defined in @@ -837,7 +877,7 @@ ___ ### useDatabaseObjectData -▸ **useDatabaseObjectData**<`T`\>(`ref`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`T`\> +▸ **useDatabaseObjectData**<`T`\>(`ref`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`T`\> #### Type parameters @@ -854,7 +894,7 @@ ___ #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`T`\> +[`ObservableStatus`](README.md#observablestatus)<`T`\> #### Defined in @@ -892,7 +932,7 @@ ___ ### useFirestoreCollection -▸ **useFirestoreCollection**<`T`\>(`query`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`QuerySnapshot`<`T`\>\> +▸ **useFirestoreCollection**<`T`\>(`query`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`QuerySnapshot`<`T`\>\> Subscribe to a Firestore collection @@ -911,7 +951,7 @@ Subscribe to a Firestore collection #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`QuerySnapshot`<`T`\>\> +[`ObservableStatus`](README.md#observablestatus)<`QuerySnapshot`<`T`\>\> #### Defined in @@ -921,7 +961,7 @@ ___ ### useFirestoreCollectionData -▸ **useFirestoreCollectionData**<`T`\>(`query`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`T`[]\> +▸ **useFirestoreCollectionData**<`T`\>(`query`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`T`[]\> Subscribe to a Firestore collection and unwrap the snapshot into an array. @@ -940,7 +980,7 @@ Subscribe to a Firestore collection and unwrap the snapshot into an array. #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`T`[]\> +[`ObservableStatus`](README.md#observablestatus)<`T`[]\> #### Defined in @@ -950,7 +990,7 @@ ___ ### useFirestoreDoc -▸ **useFirestoreDoc**<`T`\>(`ref`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`DocumentSnapshot`<`T`\>\> +▸ **useFirestoreDoc**<`T`\>(`ref`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`DocumentSnapshot`<`T`\>\> Subscribe to Firestore Document changes @@ -971,7 +1011,7 @@ You can preload data for this hook by calling `preloadFirestoreDoc` #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`DocumentSnapshot`<`T`\>\> +[`ObservableStatus`](README.md#observablestatus)<`DocumentSnapshot`<`T`\>\> #### Defined in @@ -981,7 +1021,7 @@ ___ ### useFirestoreDocData -▸ **useFirestoreDocData**<`T`\>(`ref`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`T`\> +▸ **useFirestoreDocData**<`T`\>(`ref`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`T`\> Subscribe to Firestore Document changes and unwrap the document into a plain object @@ -1000,7 +1040,7 @@ Subscribe to Firestore Document changes and unwrap the document into a plain obj #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`T`\> +[`ObservableStatus`](README.md#observablestatus)<`T`\> #### Defined in @@ -1010,7 +1050,7 @@ ___ ### useFirestoreDocDataOnce -▸ **useFirestoreDocDataOnce**<`T`\>(`ref`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`T`\> +▸ **useFirestoreDocDataOnce**<`T`\>(`ref`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`T`\> Get a Firestore document, unwrap the document into a plain object, and don't subscribe to changes @@ -1029,7 +1069,7 @@ Get a Firestore document, unwrap the document into a plain object, and don't sub #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`T`\> +[`ObservableStatus`](README.md#observablestatus)<`T`\> #### Defined in @@ -1039,7 +1079,7 @@ ___ ### useFirestoreDocOnce -▸ **useFirestoreDocOnce**<`T`\>(`ref`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`DocumentSnapshot`<`T`\>\> +▸ **useFirestoreDocOnce**<`T`\>(`ref`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`DocumentSnapshot`<`T`\>\> Get a firestore document and don't subscribe to changes @@ -1058,7 +1098,7 @@ Get a firestore document and don't subscribe to changes #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`DocumentSnapshot`<`T`\>\> +[`ObservableStatus`](README.md#observablestatus)<`DocumentSnapshot`<`T`\>\> #### Defined in @@ -1082,7 +1122,7 @@ ___ ### useIdTokenResult -▸ **useIdTokenResult**(`user`, `forceRefresh?`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`IdTokenResult`\> +▸ **useIdTokenResult**(`user`, `forceRefresh?`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`IdTokenResult`\> #### Parameters @@ -1094,7 +1134,7 @@ ___ #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`IdTokenResult`\> +[`ObservableStatus`](README.md#observablestatus)<`IdTokenResult`\> #### Defined in @@ -1104,7 +1144,7 @@ ___ ### useInitAnalytics -▸ **useInitAnalytics**(`initializer`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`Analytics`\> +▸ **useInitAnalytics**(`initializer`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`Analytics`\> #### Parameters @@ -1115,7 +1155,7 @@ ___ #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`Analytics`\> +[`ObservableStatus`](README.md#observablestatus)<`Analytics`\> #### Defined in @@ -1125,7 +1165,7 @@ ___ ### useInitAppCheck -▸ **useInitAppCheck**(`initializer`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`AppCheck`\> +▸ **useInitAppCheck**(`initializer`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`AppCheck`\> #### Parameters @@ -1136,7 +1176,7 @@ ___ #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`AppCheck`\> +[`ObservableStatus`](README.md#observablestatus)<`AppCheck`\> #### Defined in @@ -1146,7 +1186,7 @@ ___ ### useInitAuth -▸ **useInitAuth**(`initializer`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`Auth`\> +▸ **useInitAuth**(`initializer`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`Auth`\> #### Parameters @@ -1157,7 +1197,7 @@ ___ #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`Auth`\> +[`ObservableStatus`](README.md#observablestatus)<`Auth`\> #### Defined in @@ -1167,7 +1207,7 @@ ___ ### useInitDatabase -▸ **useInitDatabase**(`initializer`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`Database`\> +▸ **useInitDatabase**(`initializer`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`Database`\> #### Parameters @@ -1178,7 +1218,7 @@ ___ #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`Database`\> +[`ObservableStatus`](README.md#observablestatus)<`Database`\> #### Defined in @@ -1188,7 +1228,7 @@ ___ ### useInitFirestore -▸ **useInitFirestore**(`initializer`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`Firestore`\> +▸ **useInitFirestore**(`initializer`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`Firestore`\> #### Parameters @@ -1199,7 +1239,7 @@ ___ #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`Firestore`\> +[`ObservableStatus`](README.md#observablestatus)<`Firestore`\> #### Defined in @@ -1209,7 +1249,7 @@ ___ ### useInitFunctions -▸ **useInitFunctions**(`initializer`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`Functions`\> +▸ **useInitFunctions**(`initializer`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`Functions`\> #### Parameters @@ -1220,7 +1260,7 @@ ___ #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`Functions`\> +[`ObservableStatus`](README.md#observablestatus)<`Functions`\> #### Defined in @@ -1230,7 +1270,7 @@ ___ ### useInitPerformance -▸ **useInitPerformance**(`initializer`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`FirebasePerformance`\> +▸ **useInitPerformance**(`initializer`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`FirebasePerformance`\> #### Parameters @@ -1241,7 +1281,7 @@ ___ #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`FirebasePerformance`\> +[`ObservableStatus`](README.md#observablestatus)<`FirebasePerformance`\> #### Defined in @@ -1251,7 +1291,7 @@ ___ ### useInitRemoteConfig -▸ **useInitRemoteConfig**(`initializer`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`RemoteConfig`\> +▸ **useInitRemoteConfig**(`initializer`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`RemoteConfig`\> #### Parameters @@ -1262,7 +1302,7 @@ ___ #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`RemoteConfig`\> +[`ObservableStatus`](README.md#observablestatus)<`RemoteConfig`\> #### Defined in @@ -1272,7 +1312,7 @@ ___ ### useInitStorage -▸ **useInitStorage**(`initializer`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`FirebaseStorage`\> +▸ **useInitStorage**(`initializer`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`FirebaseStorage`\> #### Parameters @@ -1283,7 +1323,7 @@ ___ #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`FirebaseStorage`\> +[`ObservableStatus`](README.md#observablestatus)<`FirebaseStorage`\> #### Defined in @@ -1307,7 +1347,7 @@ ___ ### useObservable -▸ **useObservable**<`T`\>(`observableId`, `source`, `config?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`T`\> +▸ **useObservable**<`T`\>(`observableId`, `source`, `config?`): [`ObservableStatus`](README.md#observablestatus)<`T`\> #### Type parameters @@ -1325,11 +1365,11 @@ ___ #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`T`\> +[`ObservableStatus`](README.md#observablestatus)<`T`\> #### Defined in -[src/useObservable.ts:95](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L95) +[src/useObservable.ts:86](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L86) ___ @@ -1363,7 +1403,7 @@ ___ ### useRemoteConfigAll -▸ **useRemoteConfigAll**(`key`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`AllParameters`\> +▸ **useRemoteConfigAll**(`key`): [`ObservableStatus`](README.md#observablestatus)<`AllParameters`\> Convience method similar to useRemoteConfigValue. Returns allRemote Config parameters. @@ -1375,7 +1415,7 @@ Convience method similar to useRemoteConfigValue. Returns allRemote Config param #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`AllParameters`\> +[`ObservableStatus`](README.md#observablestatus)<`AllParameters`\> #### Defined in @@ -1385,7 +1425,7 @@ ___ ### useRemoteConfigBoolean -▸ **useRemoteConfigBoolean**(`key`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`boolean`\> +▸ **useRemoteConfigBoolean**(`key`): [`ObservableStatus`](README.md#observablestatus)<`boolean`\> Convience method similar to useRemoteConfigValue. Returns a `boolean` from a Remote Config parameter. @@ -1397,7 +1437,7 @@ Convience method similar to useRemoteConfigValue. Returns a `boolean` from a Rem #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`boolean`\> +[`ObservableStatus`](README.md#observablestatus)<`boolean`\> #### Defined in @@ -1407,7 +1447,7 @@ ___ ### useRemoteConfigNumber -▸ **useRemoteConfigNumber**(`key`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`number`\> +▸ **useRemoteConfigNumber**(`key`): [`ObservableStatus`](README.md#observablestatus)<`number`\> Convience method similar to useRemoteConfigValue. Returns a `number` from a Remote Config parameter. @@ -1419,7 +1459,7 @@ Convience method similar to useRemoteConfigValue. Returns a `number` from a Remo #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`number`\> +[`ObservableStatus`](README.md#observablestatus)<`number`\> #### Defined in @@ -1429,7 +1469,7 @@ ___ ### useRemoteConfigString -▸ **useRemoteConfigString**(`key`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`string`\> +▸ **useRemoteConfigString**(`key`): [`ObservableStatus`](README.md#observablestatus)<`string`\> Convience method similar to useRemoteConfigValue. Returns a `string` from a Remote Config parameter. @@ -1441,7 +1481,7 @@ Convience method similar to useRemoteConfigValue. Returns a `string` from a Remo #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`string`\> +[`ObservableStatus`](README.md#observablestatus)<`string`\> #### Defined in @@ -1451,7 +1491,7 @@ ___ ### useRemoteConfigValue -▸ **useRemoteConfigValue**(`key`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`RemoteConfigValue`\> +▸ **useRemoteConfigValue**(`key`): [`ObservableStatus`](README.md#observablestatus)<`RemoteConfigValue`\> Accepts a key and optionally a Remote Config instance. Returns a Remote Config Value. @@ -1464,7 +1504,7 @@ Remote Config Value. #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`RemoteConfigValue`\> +[`ObservableStatus`](README.md#observablestatus)<`RemoteConfigValue`\> #### Defined in @@ -1474,7 +1514,7 @@ ___ ### useSigninCheck -▸ **useSigninCheck**(`options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<[`SigninCheckResult`](README.md#signincheckresult)\> +▸ **useSigninCheck**(`options?`): [`ObservableStatus`](README.md#observablestatus)<[`SigninCheckResult`](README.md#signincheckresult)\> Subscribe to the signed-in status of a user. @@ -1514,7 +1554,7 @@ const {status, data: signInCheckResult} = useSigninCheck({forceRefresh: true, re #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<[`SigninCheckResult`](README.md#signincheckresult)\> +[`ObservableStatus`](README.md#observablestatus)<[`SigninCheckResult`](README.md#signincheckresult)\> #### Defined in @@ -1538,7 +1578,7 @@ ___ ### useStorageDownloadURL -▸ **useStorageDownloadURL**<`T`\>(`ref`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`string` \| `T`\> +▸ **useStorageDownloadURL**<`T`\>(`ref`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`string` \| `T`\> Subscribe to a storage ref's download URL @@ -1557,7 +1597,7 @@ Subscribe to a storage ref's download URL #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`string` \| `T`\> +[`ObservableStatus`](README.md#observablestatus)<`string` \| `T`\> #### Defined in @@ -1567,7 +1607,7 @@ ___ ### useStorageTask -▸ **useStorageTask**<`T`\>(`task`, `ref`, `options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`UploadTaskSnapshot` \| `T`\> +▸ **useStorageTask**<`T`\>(`task`, `ref`, `options?`): [`ObservableStatus`](README.md#observablestatus)<`UploadTaskSnapshot` \| `T`\> Subscribe to the progress of a storage task @@ -1587,7 +1627,7 @@ Subscribe to the progress of a storage task #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`UploadTaskSnapshot` \| `T`\> +[`ObservableStatus`](README.md#observablestatus)<`UploadTaskSnapshot` \| `T`\> #### Defined in @@ -1617,7 +1657,7 @@ ___ ### useUser -▸ **useUser**<`T`\>(`options?`): [`ObservableStatus`](interfaces/ObservableStatus.md)<`User` \| ``null``\> +▸ **useUser**<`T`\>(`options?`): [`ObservableStatus`](README.md#observablestatus)<`User` \| ``null``\> Subscribe to Firebase auth state changes, including token refresh @@ -1635,7 +1675,7 @@ Subscribe to Firebase auth state changes, including token refresh #### Returns -[`ObservableStatus`](interfaces/ObservableStatus.md)<`User` \| ``null``\> +[`ObservableStatus`](README.md#observablestatus)<`User` \| ``null``\> #### Defined in diff --git a/docs/reference/interfaces/FirebaseAppProviderProps.md b/docs/reference/interfaces/FirebaseAppProviderProps.md new file mode 100644 index 00000000..6ac494a4 --- /dev/null +++ b/docs/reference/interfaces/FirebaseAppProviderProps.md @@ -0,0 +1,52 @@ +[ReactFire reference docs](../README.md) / FirebaseAppProviderProps + +# Interface: FirebaseAppProviderProps + +## Table of contents + +### Properties + +- [appName](FirebaseAppProviderProps.md#appname) +- [firebaseApp](FirebaseAppProviderProps.md#firebaseapp) +- [firebaseConfig](FirebaseAppProviderProps.md#firebaseconfig) +- [suspense](FirebaseAppProviderProps.md#suspense) + +## Properties + +### appName + +• `Optional` **appName**: `string` + +#### Defined in + +[src/firebaseApp.tsx:15](https://github.com/FirebaseExtended/reactfire/blob/main/src/firebaseApp.tsx#L15) + +___ + +### firebaseApp + +• `Optional` **firebaseApp**: `FirebaseApp` + +#### Defined in + +[src/firebaseApp.tsx:13](https://github.com/FirebaseExtended/reactfire/blob/main/src/firebaseApp.tsx#L13) + +___ + +### firebaseConfig + +• `Optional` **firebaseConfig**: `FirebaseOptions` + +#### Defined in + +[src/firebaseApp.tsx:14](https://github.com/FirebaseExtended/reactfire/blob/main/src/firebaseApp.tsx#L14) + +___ + +### suspense + +• `Optional` **suspense**: `boolean` + +#### Defined in + +[src/firebaseApp.tsx:16](https://github.com/FirebaseExtended/reactfire/blob/main/src/firebaseApp.tsx#L16) diff --git a/docs/reference/interfaces/ObservableStatus.md b/docs/reference/interfaces/ObservableStatus.md deleted file mode 100644 index 39e4ac8e..00000000 --- a/docs/reference/interfaces/ObservableStatus.md +++ /dev/null @@ -1,102 +0,0 @@ -[ReactFire reference docs](../README.md) / ObservableStatus - -# Interface: ObservableStatus - -## Type parameters - -| Name | -| :------ | -| `T` | - -## Table of contents - -### Properties - -- [data](ObservableStatus.md#data) -- [error](ObservableStatus.md#error) -- [firstValuePromise](ObservableStatus.md#firstvaluepromise) -- [hasEmitted](ObservableStatus.md#hasemitted) -- [isComplete](ObservableStatus.md#iscomplete) -- [status](ObservableStatus.md#status) - -## Properties - -### data - -• **data**: `T` - -The most recent value. - -If `initialData` is passed in, the first value of `data` will be the valuea provided in `initialData` **UNLESS** the underlying observable is ready, in which case it will skip `initialData`. - -#### Defined in - -[src/useObservable.ts:55](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L55) - -___ - -### error - -• **error**: `undefined` \| `Error` - -Any error that may have occurred in the underlying observable - -#### Defined in - -[src/useObservable.ts:59](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L59) - -___ - -### firstValuePromise - -• **firstValuePromise**: `Promise`<`void`\> - -Promise that resolves after first emit from observable - -#### Defined in - -[src/useObservable.ts:63](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L63) - -___ - -### hasEmitted - -• **hasEmitted**: `boolean` - -Indicates whether the hook has emitted a value at some point - -If `initialData` is passed in, this will be `true`. - -#### Defined in - -[src/useObservable.ts:45](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L45) - -___ - -### isComplete - -• **isComplete**: `boolean` - -If this is `true`, the hook will be emitting no further items. - -#### Defined in - -[src/useObservable.ts:49](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L49) - -___ - -### status - -• **status**: ``"error"`` \| ``"loading"`` \| ``"success"`` - -The loading status. - -- `loading`: Waiting for the first value from an observable -- `error`: Something went wrong. Check `ObservableStatus.error` for more details -- `success`: The hook has emitted at least one value - -If `initialData` is passed in, this will skip `loading` and go straight to `success`. - -#### Defined in - -[src/useObservable.ts:39](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L39) diff --git a/docs/reference/interfaces/ObservableStatusError.md b/docs/reference/interfaces/ObservableStatusError.md new file mode 100644 index 00000000..90cde303 --- /dev/null +++ b/docs/reference/interfaces/ObservableStatusError.md @@ -0,0 +1,120 @@ +[ReactFire reference docs](../README.md) / ObservableStatusError + +# Interface: ObservableStatusError + +## Type parameters + +| Name | +| :------ | +| `T` | + +## Hierarchy + +- `ObservableStatusBase`<`T`\> + + ↳ **`ObservableStatusError`** + +## Table of contents + +### Properties + +- [data](ObservableStatusError.md#data) +- [error](ObservableStatusError.md#error) +- [firstValuePromise](ObservableStatusError.md#firstvaluepromise) +- [hasEmitted](ObservableStatusError.md#hasemitted) +- [isComplete](ObservableStatusError.md#iscomplete) +- [status](ObservableStatusError.md#status) + +## Properties + +### data + +• **data**: `undefined` \| `T` + +The most recent value. + +If `initialData` is passed in, the first value of `data` will be the valuea provided in `initialData` **UNLESS** the underlying observable is ready, in which case it will skip `initialData`. + +#### Inherited from + +ObservableStatusBase.data + +#### Defined in + +[src/useObservable.ts:56](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L56) + +___ + +### error + +• **error**: `Error` + +#### Overrides + +ObservableStatusBase.error + +#### Defined in + +[src/useObservable.ts:75](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L75) + +___ + +### firstValuePromise + +• **firstValuePromise**: `Promise`<`void`\> + +Promise that resolves after first emit from observable + +#### Inherited from + +ObservableStatusBase.firstValuePromise + +#### Defined in + +[src/useObservable.ts:64](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L64) + +___ + +### hasEmitted + +• **hasEmitted**: `boolean` + +Indicates whether the hook has emitted a value at some point + +If `initialData` is passed in, this will be `true`. + +#### Inherited from + +ObservableStatusBase.hasEmitted + +#### Defined in + +[src/useObservable.ts:46](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L46) + +___ + +### isComplete + +• **isComplete**: ``true`` + +#### Overrides + +ObservableStatusBase.isComplete + +#### Defined in + +[src/useObservable.ts:74](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L74) + +___ + +### status + +• **status**: ``"error"`` + +#### Overrides + +ObservableStatusBase.status + +#### Defined in + +[src/useObservable.ts:73](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L73) diff --git a/docs/reference/interfaces/ObservableStatusLoading.md b/docs/reference/interfaces/ObservableStatusLoading.md new file mode 100644 index 00000000..d3f0bcd7 --- /dev/null +++ b/docs/reference/interfaces/ObservableStatusLoading.md @@ -0,0 +1,116 @@ +[ReactFire reference docs](../README.md) / ObservableStatusLoading + +# Interface: ObservableStatusLoading + +## Type parameters + +| Name | +| :------ | +| `T` | + +## Hierarchy + +- `ObservableStatusBase`<`T`\> + + ↳ **`ObservableStatusLoading`** + +## Table of contents + +### Properties + +- [data](ObservableStatusLoading.md#data) +- [error](ObservableStatusLoading.md#error) +- [firstValuePromise](ObservableStatusLoading.md#firstvaluepromise) +- [hasEmitted](ObservableStatusLoading.md#hasemitted) +- [isComplete](ObservableStatusLoading.md#iscomplete) +- [status](ObservableStatusLoading.md#status) + +## Properties + +### data + +• **data**: `undefined` + +#### Overrides + +ObservableStatusBase.data + +#### Defined in + +[src/useObservable.ts:80](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L80) + +___ + +### error + +• **error**: `undefined` \| `Error` + +Any error that may have occurred in the underlying observable + +#### Inherited from + +ObservableStatusBase.error + +#### Defined in + +[src/useObservable.ts:60](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L60) + +___ + +### firstValuePromise + +• **firstValuePromise**: `Promise`<`void`\> + +Promise that resolves after first emit from observable + +#### Inherited from + +ObservableStatusBase.firstValuePromise + +#### Defined in + +[src/useObservable.ts:64](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L64) + +___ + +### hasEmitted + +• **hasEmitted**: ``false`` + +#### Overrides + +ObservableStatusBase.hasEmitted + +#### Defined in + +[src/useObservable.ts:81](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L81) + +___ + +### isComplete + +• **isComplete**: `boolean` + +If this is `true`, the hook will be emitting no further items. + +#### Inherited from + +ObservableStatusBase.isComplete + +#### Defined in + +[src/useObservable.ts:50](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L50) + +___ + +### status + +• **status**: ``"loading"`` + +#### Overrides + +ObservableStatusBase.status + +#### Defined in + +[src/useObservable.ts:79](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L79) diff --git a/docs/reference/interfaces/ObservableStatusSuccess.md b/docs/reference/interfaces/ObservableStatusSuccess.md new file mode 100644 index 00000000..4a8567f9 --- /dev/null +++ b/docs/reference/interfaces/ObservableStatusSuccess.md @@ -0,0 +1,120 @@ +[ReactFire reference docs](../README.md) / ObservableStatusSuccess + +# Interface: ObservableStatusSuccess + +## Type parameters + +| Name | +| :------ | +| `T` | + +## Hierarchy + +- `ObservableStatusBase`<`T`\> + + ↳ **`ObservableStatusSuccess`** + +## Table of contents + +### Properties + +- [data](ObservableStatusSuccess.md#data) +- [error](ObservableStatusSuccess.md#error) +- [firstValuePromise](ObservableStatusSuccess.md#firstvaluepromise) +- [hasEmitted](ObservableStatusSuccess.md#hasemitted) +- [isComplete](ObservableStatusSuccess.md#iscomplete) +- [status](ObservableStatusSuccess.md#status) + +## Properties + +### data + +• **data**: `T` + +#### Overrides + +ObservableStatusBase.data + +#### Defined in + +[src/useObservable.ts:69](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L69) + +___ + +### error + +• **error**: `undefined` \| `Error` + +Any error that may have occurred in the underlying observable + +#### Inherited from + +ObservableStatusBase.error + +#### Defined in + +[src/useObservable.ts:60](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L60) + +___ + +### firstValuePromise + +• **firstValuePromise**: `Promise`<`void`\> + +Promise that resolves after first emit from observable + +#### Inherited from + +ObservableStatusBase.firstValuePromise + +#### Defined in + +[src/useObservable.ts:64](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L64) + +___ + +### hasEmitted + +• **hasEmitted**: `boolean` + +Indicates whether the hook has emitted a value at some point + +If `initialData` is passed in, this will be `true`. + +#### Inherited from + +ObservableStatusBase.hasEmitted + +#### Defined in + +[src/useObservable.ts:46](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L46) + +___ + +### isComplete + +• **isComplete**: `boolean` + +If this is `true`, the hook will be emitting no further items. + +#### Inherited from + +ObservableStatusBase.isComplete + +#### Defined in + +[src/useObservable.ts:50](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L50) + +___ + +### status + +• **status**: ``"success"`` + +#### Overrides + +ObservableStatusBase.status + +#### Defined in + +[src/useObservable.ts:68](https://github.com/FirebaseExtended/reactfire/blob/main/src/useObservable.ts#L68) diff --git a/package-lock.json b/package-lock.json index 731fe016..bd1883d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "MIT", "dependencies": { "rxfire": "^6.0.3", - "rxjs": "^6.6.3 || ^7.0.1" + "rxjs": "^6.6.3 || ^7.0.1", + "use-sync-external-store": "^1.2.0" }, "devDependencies": { "@rollup/plugin-typescript": "^11.1.1", @@ -19,6 +20,7 @@ "@testing-library/react": "^14.0.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", + "@types/use-sync-external-store": "^0.0.3", "@typescript-eslint/eslint-plugin": "^5.60.1", "@typescript-eslint/parser": "^5.60.1", "@vitejs/plugin-react": "^4.0.1", @@ -2851,6 +2853,12 @@ "integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==", "dev": true }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -9271,8 +9279,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -9922,7 +9929,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -11898,7 +11904,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -14138,6 +14143,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index f4c5bd25..7c5e1d4f 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@testing-library/react": "^14.0.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", + "@types/use-sync-external-store": "^0.0.3", "@typescript-eslint/eslint-plugin": "^5.60.1", "@typescript-eslint/parser": "^5.60.1", "@vitejs/plugin-react": "^4.0.1", @@ -107,6 +108,7 @@ }, "dependencies": { "rxfire": "^6.0.3", - "rxjs": "^6.6.3 || ^7.0.1" + "rxjs": "^6.6.3 || ^7.0.1", + "use-sync-external-store": "^1.2.0" } } diff --git a/src/SuspenseSubject.ts b/src/SuspenseSubject.ts index 40b05ec2..32d01a2c 100644 --- a/src/SuspenseSubject.ts +++ b/src/SuspenseSubject.ts @@ -1,5 +1,6 @@ import { empty, Observable, Subject, Subscriber, Subscription } from 'rxjs'; import { catchError, shareReplay, tap } from 'rxjs/operators'; +import { ObservableStatus } from './useObservable'; export class SuspenseSubject extends Subject { private _value: T | undefined; @@ -9,6 +10,8 @@ export class SuspenseSubject extends Subject { private _error: any = undefined; private _innerObservable: Observable; private _warmupSubscription: Subscription; + private _immutableStatus: ObservableStatus; + private _isComplete = false; // @ts-expect-error: TODO: double check to see if this is an RXJS thing or if we should listen to TS private _innerSubscriber: Subscription; @@ -18,6 +21,16 @@ export class SuspenseSubject extends Subject { constructor(innerObservable: Observable, private _timeoutWindow: number, private _suspenseEnabled: boolean) { super(); this._firstEmission = new Promise((resolve) => (this._resolveFirstEmission = resolve)); + + this._immutableStatus = { + status: 'loading', + hasEmitted: false, + isComplete: false, + data: undefined, + error: undefined, + firstValuePromise: this._firstEmission + }; + this._innerObservable = innerObservable.pipe( tap({ next: (v) => { @@ -28,7 +41,12 @@ export class SuspenseSubject extends Subject { // resolve the promise, so suspense tries again this._error = e; this._resolveFirstEmission(); + this._updateImmutableStatus(); }, + complete: () => { + this._isComplete = true; + this._updateImmutableStatus(); + } }), catchError(() => empty()), shareReplay(1) @@ -69,10 +87,27 @@ export class SuspenseSubject extends Subject { return this._firstEmission; } + private _updateImmutableStatus() { + // @ts-expect-error + // TS fails here because ObservableStatus defines specific + // relationships between the fields. This is difficult to + // code for here, so the relationships between the ObservableStatus fields + // are mostly checked in tests instead + this._immutableStatus = { + status: this._error ? 'error' : (this._hasValue ? 'success' : 'loading'), + hasEmitted: this._hasValue, + isComplete: this._isComplete, + data: this._value, + error: this._error, + firstValuePromise: this._firstEmission + }; + } + private _next(value: T) { this._hasValue = true; this._value = value; this._resolveFirstEmission(); + this._updateImmutableStatus(); } private _reset() { @@ -84,6 +119,7 @@ export class SuspenseSubject extends Subject { this._value = undefined; this._error = undefined; this._firstEmission = new Promise((resolve) => (this._resolveFirstEmission = resolve)); + this._updateImmutableStatus(); } _subscribe(subscriber: Subscriber): Subscription { @@ -97,4 +133,8 @@ export class SuspenseSubject extends Subject { get ourError() { return this._error; } + + get immutableStatus() { + return this._immutableStatus; + } } diff --git a/src/auth.tsx b/src/auth.tsx index ca00a6c9..d535b894 100644 --- a/src/auth.tsx +++ b/src/auth.tsx @@ -201,7 +201,14 @@ function getClaimsObjectValidator(requiredClaims: Claims): ClaimsValidator { * Meant for Concurrent mode only (``). [More detail](https://github.com/FirebaseExtended/reactfire/issues/325#issuecomment-827654376). */ export function ClaimsCheck({ user, fallback, children, requiredClaims }: ClaimsCheckProps) { - const { data } = useIdTokenResult(user, false); + const { data, status, error } = useIdTokenResult(user, false); + + if (status === 'loading') { + throw new Error('ClaimsCheck must be run in Suspense mode'); + } else if (status === 'error') { + throw error + } + const { claims } = data; const missingClaims: { [key: string]: { expected: string; actual: string | undefined } } = {}; diff --git a/src/firebaseApp.tsx b/src/firebaseApp.tsx index a4e73dc3..d7f2d1e9 100644 --- a/src/firebaseApp.tsx +++ b/src/firebaseApp.tsx @@ -9,7 +9,7 @@ const DEFAULT_APP_NAME = '[DEFAULT]'; const FirebaseAppContext = React.createContext(undefined); const SuspenseEnabledContext = React.createContext(false); -interface FirebaseAppProviderProps { +export interface FirebaseAppProviderProps { firebaseApp?: FirebaseApp; firebaseConfig?: FirebaseOptions; appName?: string; diff --git a/src/storage.tsx b/src/storage.tsx index c3aa1cd7..90ea56c1 100644 --- a/src/storage.tsx +++ b/src/storage.tsx @@ -33,7 +33,7 @@ export function useStorageDownloadURL(ref: StorageReference, options return useObservable(observableId, observable$, options); } -type StorageImageProps = { +export type StorageImageProps = { storagePath: string; storage?: FirebaseStorage; suspense?: boolean; diff --git a/src/useObservable.ts b/src/useObservable.ts index e450055b..08e8c748 100644 --- a/src/useObservable.ts +++ b/src/useObservable.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { useSyncExternalStore } from 'use-sync-external-store/shim'; import { Observable } from 'rxjs'; import { SuspenseSubject } from './SuspenseSubject'; import { useSuspenseEnabledFromConfigAndContext } from './firebaseApp'; @@ -26,7 +27,7 @@ export function preloadObservable(source: Observable, id: string, suspense } } -export interface ObservableStatus { +interface ObservableStatusBase { /** * The loading status. * @@ -52,7 +53,7 @@ export interface ObservableStatus { * * If `initialData` is passed in, the first value of `data` will be the valuea provided in `initialData` **UNLESS** the underlying observable is ready, in which case it will skip `initialData`. */ - data: T; + data: T | undefined; /** * Any error that may have occurred in the underlying observable */ @@ -63,42 +64,33 @@ export interface ObservableStatus { firstValuePromise: Promise; } -function reducerFactory(observable: SuspenseSubject) { - return function reducer(state: ObservableStatus, action: 'value' | 'error' | 'complete'): ObservableStatus { - // always make sure these values are in sync with the observable - const newState = { - ...state, - hasEmitted: state.hasEmitted || observable.hasValue, - error: observable.ourError, - firstValuePromise: observable.firstEmission, - }; - if (observable.hasValue) { - newState.data = observable.value; - } - - switch (action) { - case 'value': - newState.status = 'success'; - return newState; - case 'error': - newState.status = 'error'; - return newState; - case 'complete': - newState.isComplete = true; - return newState; - default: - throw new Error(`invalid action "${action}"`); - } - }; +export interface ObservableStatusSuccess extends ObservableStatusBase { + status: 'success'; + data: T; } +export interface ObservableStatusError extends ObservableStatusBase { + status: 'error'; + isComplete: true; + error: Error; +} + +export interface ObservableStatusLoading extends ObservableStatusBase { + status: 'loading'; + data: undefined; + hasEmitted: false; +} + +export type ObservableStatus = ObservableStatusLoading | ObservableStatusError | ObservableStatusSuccess; + export function useObservable(observableId: string, source: Observable, config: ReactFireOptions = {}): ObservableStatus { - // Register the observable with the cache if (!observableId) { throw new Error('cannot call useObservable without an observableId'); } + const suspenseEnabled = useSuspenseEnabledFromConfigAndContext(config.suspense); + // Register the observable with the cache const observable = preloadObservable(source, observableId, suspenseEnabled); // Suspend if suspense is enabled and no initial data exists @@ -108,31 +100,43 @@ export function useObservable(observableId: string, source: Observa throw observable.firstEmission; } - const initialState: ObservableStatus = { - status: hasData ? 'success' : 'loading', - hasEmitted: hasData, - isComplete: false, - data: observable.hasValue ? observable.value : config?.initialData ?? config?.startWithValue, - error: observable.ourError, - firstValuePromise: observable.firstEmission, - }; - const [status, dispatch] = React.useReducer, 'value' | 'error' | 'complete'>>(reducerFactory(observable), initialState); - - React.useEffect(() => { - const subscription = observable.subscribe({ - next: () => { - dispatch('value'); - }, - error: (e) => { - dispatch('error'); - throw e; - }, - complete: () => { - dispatch('complete'); - }, - }); - return () => subscription.unsubscribe(); + const subscribe = React.useCallback((onStoreChange: () => void) => { + const subscription = observable.subscribe({ + next: () => { + onStoreChange(); + }, + error: (e) => { + onStoreChange(); + throw e; + }, + complete: () => { + onStoreChange(); + }, + }); + + return () => { + subscription.unsubscribe(); + } + }, [observable]) + + const getSnapshot = React.useCallback<() => ObservableStatus>(() => { + return observable.immutableStatus; }, [observable]); - return status; + const update = useSyncExternalStore(subscribe, getSnapshot); + + // modify the value if initialData exists + if (!observable.hasValue && hasData) { + update.data = config?.initialData ?? config?.startWithValue; + update.status = 'success'; + update.hasEmitted = true; + } + + // throw an error if there is an error + // TODO(jhuleatt) this is the current, tested-for, behavior. But do we actually want it? + if (update.error) { + throw update.error; + } + + return update; } diff --git a/test/firestore.test.tsx b/test/firestore.test.tsx index 16ca77e2..f83aee19 100644 --- a/test/firestore.test.tsx +++ b/test/firestore.test.tsx @@ -17,6 +17,7 @@ import { baseConfig } from './appConfig'; import { randomString } from './test-utils'; import { addDoc, collection, doc, getFirestore, query, setDoc, connectFirestoreEmulator, where, getDoc } from 'firebase/firestore'; +import type { DocumentReference } from 'firebase/firestore'; describe('Firestore', () => { const app = initializeApp(baseConfig, 'firestore-test-suite'); @@ -99,6 +100,50 @@ describe('Firestore', () => { expect(result.current.status).toEqual('success'); expect(result.current.data).toBeUndefined(); }); + + it('goes back into a loading state if you swap the query', async () => { + const mockData = { a: 'hello' }; + const otherMockData = { a: 'goodbye' }; + + const collectionRef = collection(db, randomString()); + const firstRef = doc(collectionRef, randomString()); + const secondRef = doc(collectionRef, randomString()); + + await setDoc(firstRef, mockData); + await setDoc(secondRef, otherMockData); + + const { result, rerender } = renderHook( + (props: { docRef: DocumentReference }) => { + const update = useFirestoreDocData(props.docRef, { idField: 'id' }); + + // important!! check that the hook doesn't show stale data + // for example, if props.docRef.id is a new ref but update.data.id is data for the old ref + if (update.status === 'success') { + expect(update.data.id).toEqual(props.docRef.id); + } + + return update; + }, + { + wrapper: Provider, + initialProps: { docRef: firstRef }, + } + ); + + // ensure first ref's data is loaded + await waitFor(() => expect(result.current.status).toEqual('success')); + expect(result.current.data).toBeDefined(); + expect(result.current.data.a).toEqual(mockData.a); + + // re-render the hook with the second reference + rerender({ docRef: secondRef }); + + // ensure second ref's data is loaded + await waitFor(() => { + expect(result.current.data).toBeDefined(); + expect(result.current.data.a).toEqual(otherMockData.a); + }); + }); }); describe('useFirestoreDocOnce', () => { diff --git a/test/useObservable.test.tsx b/test/useObservable.test.tsx index 5869e589..c00bd333 100644 --- a/test/useObservable.test.tsx +++ b/test/useObservable.test.tsx @@ -114,6 +114,20 @@ describe('useObservable', () => { await findByTestId('comp'); expect(element).toHaveTextContent(startVal); }); + + it('sets isComplete', async () => { + const observable$: Subject = new Subject(); + + const { result } = renderHook(() => useObservable('isComplete test', observable$, { suspense: false })); + + expect(result.current.isComplete).toEqual(false); + + act(() => observable$.next('val')); + expect(result.current.isComplete).toEqual(false); + + act(() => observable$.complete()); + await waitFor(() => expect(result.current.isComplete).toEqual(true)); + }); }); describe('Suspense Mode', () => {