reactfire 4.2.3-exp.6734b6a → 4.2.3-exp.85cb2bb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/SuspenseSubject.d.ts +7 -1
- package/dist/auth.d.ts +3 -3
- package/dist/firebaseApp.d.ts +2 -3
- package/dist/firestore.d.ts +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +963 -895
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +36 -20
- package/dist/index.umd.cjs.map +1 -1
- package/dist/sdk.d.ts +11 -11
- package/dist/storage.d.ts +2 -3
- package/dist/useObservable.d.ts +19 -3
- package/package.json +34 -33
- package/src/SuspenseSubject.ts +47 -2
- package/src/auth.tsx +10 -3
- package/src/firebaseApp.tsx +2 -2
- package/src/firestore.tsx +5 -5
- package/src/storage.tsx +3 -3
- package/src/useObservable.ts +64 -59
package/dist/sdk.d.ts
CHANGED
|
@@ -20,34 +20,34 @@ export declare const FunctionsSdkContext: React.Context<Functions | undefined>;
|
|
|
20
20
|
export declare const StorageSdkContext: React.Context<FirebaseStorage | undefined>;
|
|
21
21
|
export declare const PerformanceSdkContext: React.Context<FirebasePerformance | undefined>;
|
|
22
22
|
export declare const RemoteConfigSdkContext: React.Context<RemoteConfig | undefined>;
|
|
23
|
-
|
|
23
|
+
type FirebaseSdks = Analytics | AppCheck | Auth | Database | Firestore | FirebasePerformance | FirebaseStorage | Functions | RemoteConfig;
|
|
24
24
|
export declare const AppCheckProvider: (props: React.PropsWithChildren<{
|
|
25
25
|
sdk: AppCheck;
|
|
26
|
-
}>) => JSX.Element;
|
|
26
|
+
}>) => React.JSX.Element;
|
|
27
27
|
export declare const AuthProvider: (props: React.PropsWithChildren<{
|
|
28
28
|
sdk: Auth;
|
|
29
|
-
}>) => JSX.Element;
|
|
29
|
+
}>) => React.JSX.Element;
|
|
30
30
|
export declare const AnalyticsProvider: (props: React.PropsWithChildren<{
|
|
31
31
|
sdk: Analytics;
|
|
32
|
-
}>) => JSX.Element;
|
|
32
|
+
}>) => React.JSX.Element;
|
|
33
33
|
export declare const DatabaseProvider: (props: React.PropsWithChildren<{
|
|
34
34
|
sdk: Database;
|
|
35
|
-
}>) => JSX.Element;
|
|
35
|
+
}>) => React.JSX.Element;
|
|
36
36
|
export declare const FirestoreProvider: (props: React.PropsWithChildren<{
|
|
37
37
|
sdk: Firestore;
|
|
38
|
-
}>) => JSX.Element;
|
|
38
|
+
}>) => React.JSX.Element;
|
|
39
39
|
export declare const FunctionsProvider: (props: React.PropsWithChildren<{
|
|
40
40
|
sdk: Functions;
|
|
41
|
-
}>) => JSX.Element;
|
|
41
|
+
}>) => React.JSX.Element;
|
|
42
42
|
export declare const PerformanceProvider: (props: React.PropsWithChildren<{
|
|
43
43
|
sdk: FirebasePerformance;
|
|
44
|
-
}>) => JSX.Element;
|
|
44
|
+
}>) => React.JSX.Element;
|
|
45
45
|
export declare const StorageProvider: (props: React.PropsWithChildren<{
|
|
46
46
|
sdk: FirebaseStorage;
|
|
47
|
-
}>) => JSX.Element;
|
|
47
|
+
}>) => React.JSX.Element;
|
|
48
48
|
export declare const RemoteConfigProvider: (props: React.PropsWithChildren<{
|
|
49
49
|
sdk: RemoteConfig;
|
|
50
|
-
}>) => JSX.Element;
|
|
50
|
+
}>) => React.JSX.Element;
|
|
51
51
|
export declare const useAppCheck: () => AppCheck;
|
|
52
52
|
export declare const useAuth: () => Auth;
|
|
53
53
|
export declare const useAnalytics: () => Analytics;
|
|
@@ -57,7 +57,7 @@ export declare const useFunctions: () => Functions;
|
|
|
57
57
|
export declare const usePerformance: () => FirebasePerformance;
|
|
58
58
|
export declare const useStorage: () => FirebaseStorage;
|
|
59
59
|
export declare const useRemoteConfig: () => RemoteConfig;
|
|
60
|
-
|
|
60
|
+
type InitSdkHook<Sdk extends FirebaseSdks> = (initializer: (firebaseApp: FirebaseApp) => Promise<Sdk>, options?: ReactFireOptions<Sdk>) => ObservableStatus<Sdk>;
|
|
61
61
|
export declare const useInitAppCheck: InitSdkHook<AppCheck>;
|
|
62
62
|
export declare const useInitAuth: InitSdkHook<Auth>;
|
|
63
63
|
export declare const useInitAnalytics: InitSdkHook<Analytics>;
|
package/dist/storage.d.ts
CHANGED
|
@@ -16,11 +16,10 @@ export declare function useStorageTask<T = unknown>(task: UploadTask, ref: Stora
|
|
|
16
16
|
* @param options
|
|
17
17
|
*/
|
|
18
18
|
export declare function useStorageDownloadURL<T = string>(ref: StorageReference, options?: ReactFireOptions<T>): ObservableStatus<string | T>;
|
|
19
|
-
|
|
19
|
+
export type StorageImageProps = {
|
|
20
20
|
storagePath: string;
|
|
21
21
|
storage?: FirebaseStorage;
|
|
22
22
|
suspense?: boolean;
|
|
23
23
|
placeHolder?: JSX.Element;
|
|
24
24
|
};
|
|
25
|
-
export declare function StorageImage(props: StorageImageProps & React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>): JSX.Element;
|
|
26
|
-
export {};
|
|
25
|
+
export declare function StorageImage(props: StorageImageProps & React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>): React.JSX.Element;
|
package/dist/useObservable.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
2
|
import { SuspenseSubject } from './SuspenseSubject';
|
|
3
3
|
import { ReactFireOptions } from './';
|
|
4
|
-
export declare function preloadObservable<T>(source: Observable<T>, id: string): SuspenseSubject<T>;
|
|
5
|
-
|
|
4
|
+
export declare function preloadObservable<T>(source: Observable<T>, id: string, suspenseEnabled?: boolean): SuspenseSubject<T>;
|
|
5
|
+
interface ObservableStatusBase<T> {
|
|
6
6
|
/**
|
|
7
7
|
* The loading status.
|
|
8
8
|
*
|
|
@@ -28,7 +28,7 @@ export interface ObservableStatus<T> {
|
|
|
28
28
|
*
|
|
29
29
|
* 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`.
|
|
30
30
|
*/
|
|
31
|
-
data: T;
|
|
31
|
+
data: T | undefined;
|
|
32
32
|
/**
|
|
33
33
|
* Any error that may have occurred in the underlying observable
|
|
34
34
|
*/
|
|
@@ -38,4 +38,20 @@ export interface ObservableStatus<T> {
|
|
|
38
38
|
*/
|
|
39
39
|
firstValuePromise: Promise<void>;
|
|
40
40
|
}
|
|
41
|
+
export interface ObservableStatusSuccess<T> extends ObservableStatusBase<T> {
|
|
42
|
+
status: 'success';
|
|
43
|
+
data: T;
|
|
44
|
+
}
|
|
45
|
+
export interface ObservableStatusError<T> extends ObservableStatusBase<T> {
|
|
46
|
+
status: 'error';
|
|
47
|
+
isComplete: true;
|
|
48
|
+
error: Error;
|
|
49
|
+
}
|
|
50
|
+
export interface ObservableStatusLoading<T> extends ObservableStatusBase<T> {
|
|
51
|
+
status: 'loading';
|
|
52
|
+
data: undefined;
|
|
53
|
+
hasEmitted: false;
|
|
54
|
+
}
|
|
55
|
+
export type ObservableStatus<T> = ObservableStatusLoading<T> | ObservableStatusError<T> | ObservableStatusSuccess<T>;
|
|
41
56
|
export declare function useObservable<T = unknown>(observableId: string, source: Observable<T>, config?: ReactFireOptions): ObservableStatus<T>;
|
|
57
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "4.2.3-exp.
|
|
2
|
+
"version": "4.2.3-exp.85cb2bb",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.umd.cjs",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"docs:fork": "typedoc --options typedoc.json --gitRemote upstream && markdown-toc -i docs/use.md"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"firebase": "^9.0.0 ||
|
|
39
|
+
"firebase": "^9.0.0 || next",
|
|
40
40
|
"react": ">=16 || experimental"
|
|
41
41
|
},
|
|
42
42
|
"husky": {
|
|
@@ -71,43 +71,44 @@
|
|
|
71
71
|
}
|
|
72
72
|
],
|
|
73
73
|
"devDependencies": {
|
|
74
|
-
"@rollup/plugin-typescript": "^
|
|
75
|
-
"@size-limit/preset-small-lib": "^
|
|
76
|
-
"@testing-library/jest-dom": "^5.16.
|
|
77
|
-
"@testing-library/react": "^
|
|
78
|
-
"@
|
|
79
|
-
"@types/react": "^
|
|
80
|
-
"@types/
|
|
81
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
82
|
-
"@typescript-eslint/parser": "^5.
|
|
83
|
-
"@vitejs/plugin-react": "^
|
|
84
|
-
"@vitest/ui": "^0.
|
|
85
|
-
"cross-fetch": "^3.1.
|
|
86
|
-
"eslint": "^8.
|
|
87
|
-
"eslint-plugin-no-only-tests": "^
|
|
88
|
-
"eslint-plugin-react": "^7.
|
|
74
|
+
"@rollup/plugin-typescript": "^11.1.1",
|
|
75
|
+
"@size-limit/preset-small-lib": "^8.2.6",
|
|
76
|
+
"@testing-library/jest-dom": "^5.16.5",
|
|
77
|
+
"@testing-library/react": "^14.0.0",
|
|
78
|
+
"@types/react": "^18.2.0",
|
|
79
|
+
"@types/react-dom": "^18.2.0",
|
|
80
|
+
"@types/use-sync-external-store": "^0.0.3",
|
|
81
|
+
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
|
82
|
+
"@typescript-eslint/parser": "^5.60.1",
|
|
83
|
+
"@vitejs/plugin-react": "^4.0.1",
|
|
84
|
+
"@vitest/ui": "^0.32.2",
|
|
85
|
+
"cross-fetch": "^3.1.6",
|
|
86
|
+
"eslint": "^8.43.0",
|
|
87
|
+
"eslint-plugin-no-only-tests": "^3.1.0",
|
|
88
|
+
"eslint-plugin-react": "^7.32.2",
|
|
89
89
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
90
90
|
"firebase": "^9.9.0",
|
|
91
|
-
"firebase-tools": "^
|
|
91
|
+
"firebase-tools": "^12.4.0",
|
|
92
92
|
"globalthis": "^1.0.3",
|
|
93
|
-
"husky": "^8.0.
|
|
94
|
-
"jest-environment-jsdom": "^
|
|
95
|
-
"jsdom": "^
|
|
93
|
+
"husky": "^8.0.3",
|
|
94
|
+
"jest-environment-jsdom": "^29.5.0",
|
|
95
|
+
"jsdom": "^22.1.0",
|
|
96
96
|
"markdown-toc": "^1.2.0",
|
|
97
|
-
"prettier": "^2.
|
|
98
|
-
"react": "^
|
|
99
|
-
"react-dom": "^
|
|
100
|
-
"react-test-renderer": "^
|
|
101
|
-
"rollup-plugin-visualizer": "^5.
|
|
102
|
-
"size-limit": "^
|
|
103
|
-
"typedoc": "^0.
|
|
104
|
-
"typedoc-plugin-markdown": "^3.
|
|
105
|
-
"typescript": "^
|
|
106
|
-
"vite": "^3.
|
|
107
|
-
"vitest": "^0.
|
|
97
|
+
"prettier": "^2.8.8",
|
|
98
|
+
"react": "^18.2.0",
|
|
99
|
+
"react-dom": "^18.2.0",
|
|
100
|
+
"react-test-renderer": "^18.2.0",
|
|
101
|
+
"rollup-plugin-visualizer": "^5.9.2",
|
|
102
|
+
"size-limit": "^8.2.6",
|
|
103
|
+
"typedoc": "^0.24.8",
|
|
104
|
+
"typedoc-plugin-markdown": "^3.15.3",
|
|
105
|
+
"typescript": "^5.1.3",
|
|
106
|
+
"vite": "^4.3.9",
|
|
107
|
+
"vitest": "^0.32.2"
|
|
108
108
|
},
|
|
109
109
|
"dependencies": {
|
|
110
110
|
"rxfire": "^6.0.3",
|
|
111
|
-
"rxjs": "^6.6.3 || ^7.0.1"
|
|
111
|
+
"rxjs": "^6.6.3 || ^7.0.1",
|
|
112
|
+
"use-sync-external-store": "^1.2.0"
|
|
112
113
|
}
|
|
113
114
|
}
|
package/src/SuspenseSubject.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { empty, Observable, Subject, Subscriber, Subscription } from 'rxjs';
|
|
2
2
|
import { catchError, shareReplay, tap } from 'rxjs/operators';
|
|
3
|
+
import { ObservableStatus } from './useObservable';
|
|
3
4
|
|
|
4
5
|
export class SuspenseSubject<T> extends Subject<T> {
|
|
5
6
|
private _value: T | undefined;
|
|
@@ -9,15 +10,27 @@ export class SuspenseSubject<T> extends Subject<T> {
|
|
|
9
10
|
private _error: any = undefined;
|
|
10
11
|
private _innerObservable: Observable<T>;
|
|
11
12
|
private _warmupSubscription: Subscription;
|
|
13
|
+
private _immutableStatus: ObservableStatus<T>;
|
|
14
|
+
private _isComplete = false;
|
|
12
15
|
|
|
13
16
|
// @ts-expect-error: TODO: double check to see if this is an RXJS thing or if we should listen to TS
|
|
14
17
|
private _innerSubscriber: Subscription;
|
|
15
18
|
// @ts-expect-error: TODO: double check to see if this is an RXJS thing or if we should listen to TS
|
|
16
19
|
private _resolveFirstEmission: () => void;
|
|
17
20
|
|
|
18
|
-
constructor(innerObservable: Observable<T>, private _timeoutWindow: number) {
|
|
21
|
+
constructor(innerObservable: Observable<T>, private _timeoutWindow: number, private _suspenseEnabled: boolean) {
|
|
19
22
|
super();
|
|
20
23
|
this._firstEmission = new Promise<void>((resolve) => (this._resolveFirstEmission = resolve));
|
|
24
|
+
|
|
25
|
+
this._immutableStatus = {
|
|
26
|
+
status: 'loading',
|
|
27
|
+
hasEmitted: false,
|
|
28
|
+
isComplete: false,
|
|
29
|
+
data: undefined,
|
|
30
|
+
error: undefined,
|
|
31
|
+
firstValuePromise: this._firstEmission
|
|
32
|
+
};
|
|
33
|
+
|
|
21
34
|
this._innerObservable = innerObservable.pipe(
|
|
22
35
|
tap({
|
|
23
36
|
next: (v) => {
|
|
@@ -28,7 +41,12 @@ export class SuspenseSubject<T> extends Subject<T> {
|
|
|
28
41
|
// resolve the promise, so suspense tries again
|
|
29
42
|
this._error = e;
|
|
30
43
|
this._resolveFirstEmission();
|
|
44
|
+
this._updateImmutableStatus();
|
|
31
45
|
},
|
|
46
|
+
complete: () => {
|
|
47
|
+
this._isComplete = true;
|
|
48
|
+
this._updateImmutableStatus();
|
|
49
|
+
}
|
|
32
50
|
}),
|
|
33
51
|
catchError(() => empty()),
|
|
34
52
|
shareReplay(1)
|
|
@@ -38,7 +56,12 @@ export class SuspenseSubject<T> extends Subject<T> {
|
|
|
38
56
|
|
|
39
57
|
// set a timeout for resetting the cache, subscriptions will cancel the timeout
|
|
40
58
|
// and reschedule again on unsubscribe
|
|
41
|
-
|
|
59
|
+
if (this._suspenseEnabled) {
|
|
60
|
+
// Noop if suspense is enabled
|
|
61
|
+
this._timeoutHandler = setTimeout(() => {}, this._timeoutWindow);
|
|
62
|
+
} else {
|
|
63
|
+
this._timeoutHandler = setTimeout(this._reset.bind(this), this._timeoutWindow);
|
|
64
|
+
}
|
|
42
65
|
}
|
|
43
66
|
|
|
44
67
|
get hasValue(): boolean {
|
|
@@ -64,10 +87,27 @@ export class SuspenseSubject<T> extends Subject<T> {
|
|
|
64
87
|
return this._firstEmission;
|
|
65
88
|
}
|
|
66
89
|
|
|
90
|
+
private _updateImmutableStatus() {
|
|
91
|
+
// @ts-expect-error
|
|
92
|
+
// TS fails here because ObservableStatus defines specific
|
|
93
|
+
// relationships between the fields. This is difficult to
|
|
94
|
+
// code for here, so the relationships between the ObservableStatus fields
|
|
95
|
+
// are mostly checked in tests instead
|
|
96
|
+
this._immutableStatus = {
|
|
97
|
+
status: this._error ? 'error' : (this._hasValue ? 'success' : 'loading'),
|
|
98
|
+
hasEmitted: this._hasValue,
|
|
99
|
+
isComplete: this._isComplete,
|
|
100
|
+
data: this._value,
|
|
101
|
+
error: this._error,
|
|
102
|
+
firstValuePromise: this._firstEmission
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
67
106
|
private _next(value: T) {
|
|
68
107
|
this._hasValue = true;
|
|
69
108
|
this._value = value;
|
|
70
109
|
this._resolveFirstEmission();
|
|
110
|
+
this._updateImmutableStatus();
|
|
71
111
|
}
|
|
72
112
|
|
|
73
113
|
private _reset() {
|
|
@@ -79,6 +119,7 @@ export class SuspenseSubject<T> extends Subject<T> {
|
|
|
79
119
|
this._value = undefined;
|
|
80
120
|
this._error = undefined;
|
|
81
121
|
this._firstEmission = new Promise<void>((resolve) => (this._resolveFirstEmission = resolve));
|
|
122
|
+
this._updateImmutableStatus();
|
|
82
123
|
}
|
|
83
124
|
|
|
84
125
|
_subscribe(subscriber: Subscriber<T>): Subscription {
|
|
@@ -92,4 +133,8 @@ export class SuspenseSubject<T> extends Subject<T> {
|
|
|
92
133
|
get ourError() {
|
|
93
134
|
return this._error;
|
|
94
135
|
}
|
|
136
|
+
|
|
137
|
+
get immutableStatus() {
|
|
138
|
+
return this._immutableStatus;
|
|
139
|
+
}
|
|
95
140
|
}
|
package/src/auth.tsx
CHANGED
|
@@ -28,7 +28,7 @@ export function useUser<T = unknown>(options?: ReactFireOptions<T>): ObservableS
|
|
|
28
28
|
return useObservable(observableId, observable$, options);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export function useIdTokenResult(user: User, forceRefresh
|
|
31
|
+
export function useIdTokenResult(user: User, forceRefresh = false, options?: ReactFireOptions<IdTokenResult>): ObservableStatus<IdTokenResult> {
|
|
32
32
|
if (!user) {
|
|
33
33
|
throw new Error('you must provide a user');
|
|
34
34
|
}
|
|
@@ -173,7 +173,7 @@ export function useSigninCheck(
|
|
|
173
173
|
})
|
|
174
174
|
);
|
|
175
175
|
|
|
176
|
-
return useObservable(observableId, observable);
|
|
176
|
+
return useObservable(observableId, observable, options);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
function getClaimsObjectValidator(requiredClaims: Claims): ClaimsValidator {
|
|
@@ -201,7 +201,14 @@ function getClaimsObjectValidator(requiredClaims: Claims): ClaimsValidator {
|
|
|
201
201
|
* Meant for Concurrent mode only (`<FirebaseAppProvider suspense=true />`). [More detail](https://github.com/FirebaseExtended/reactfire/issues/325#issuecomment-827654376).
|
|
202
202
|
*/
|
|
203
203
|
export function ClaimsCheck({ user, fallback, children, requiredClaims }: ClaimsCheckProps) {
|
|
204
|
-
const { data } = useIdTokenResult(user, false);
|
|
204
|
+
const { data, status, error } = useIdTokenResult(user, false);
|
|
205
|
+
|
|
206
|
+
if (status === 'loading') {
|
|
207
|
+
throw new Error('ClaimsCheck must be run in Suspense mode');
|
|
208
|
+
} else if (status === 'error') {
|
|
209
|
+
throw error
|
|
210
|
+
}
|
|
211
|
+
|
|
205
212
|
const { claims } = data;
|
|
206
213
|
const missingClaims: { [key: string]: { expected: string; actual: string | undefined } } = {};
|
|
207
214
|
|
package/src/firebaseApp.tsx
CHANGED
|
@@ -9,7 +9,7 @@ const DEFAULT_APP_NAME = '[DEFAULT]';
|
|
|
9
9
|
const FirebaseAppContext = React.createContext<FirebaseApp | undefined>(undefined);
|
|
10
10
|
const SuspenseEnabledContext = React.createContext<boolean>(false);
|
|
11
11
|
|
|
12
|
-
interface FirebaseAppProviderProps {
|
|
12
|
+
export interface FirebaseAppProviderProps {
|
|
13
13
|
firebaseApp?: FirebaseApp;
|
|
14
14
|
firebaseConfig?: FirebaseOptions;
|
|
15
15
|
appName?: string;
|
|
@@ -65,7 +65,7 @@ export function useIsSuspenseEnabled(): boolean {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export function useSuspenseEnabledFromConfigAndContext(suspenseFromConfig?: boolean): boolean {
|
|
68
|
-
|
|
68
|
+
const suspenseFromContext = React.useContext(SuspenseEnabledContext);
|
|
69
69
|
|
|
70
70
|
// prioritize config over context
|
|
71
71
|
if (suspenseFromConfig !== undefined) {
|
package/src/firestore.tsx
CHANGED
|
@@ -6,13 +6,13 @@ import { first } from 'rxjs/operators';
|
|
|
6
6
|
import { Query as FirestoreQuery, QuerySnapshot, DocumentReference, queryEqual, DocumentData, DocumentSnapshot } from 'firebase/firestore';
|
|
7
7
|
|
|
8
8
|
// Since we're side-effect free, we need to ensure our observableId cache is global
|
|
9
|
-
const cachedQueries: Array<FirestoreQuery
|
|
9
|
+
const cachedQueries: Array<FirestoreQuery<any>> = (globalThis as any as ReactFireGlobals)._reactFireFirestoreQueryCache || [];
|
|
10
10
|
|
|
11
11
|
if (!(globalThis as any as ReactFireGlobals)._reactFireFirestoreQueryCache) {
|
|
12
12
|
(globalThis as any as ReactFireGlobals)._reactFireFirestoreQueryCache = cachedQueries;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function getUniqueIdForFirestoreQuery(query: FirestoreQuery) {
|
|
15
|
+
function getUniqueIdForFirestoreQuery<T = DocumentData>(query: FirestoreQuery<T>) {
|
|
16
16
|
const index = cachedQueries.findIndex((cachedQuery) => queryEqual(cachedQuery, query));
|
|
17
17
|
if (index > -1) {
|
|
18
18
|
return index;
|
|
@@ -30,12 +30,12 @@ export async function preloadFirestoreDoc(refProvider: () => Promise<DocumentRef
|
|
|
30
30
|
return preloadObservable(doc(ref), getDocObservableId(ref));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function getDocObservableId(ref: DocumentReference) {
|
|
33
|
+
function getDocObservableId<T = DocumentData>(ref: DocumentReference<T>) {
|
|
34
34
|
return `firestore:doc:${ref.firestore.app.name}:${ref.path}`;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* Subscribe to Firestore Document changes
|
|
39
39
|
*
|
|
40
40
|
* You can preload data for this hook by calling `preloadFirestoreDoc`
|
|
41
41
|
*/
|
|
@@ -57,7 +57,7 @@ export function useFirestoreDocOnce<T = DocumentData>(ref: DocumentReference<T>,
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
60
|
+
* Subscribe to Firestore Document changes and unwrap the document into a plain object
|
|
61
61
|
*/
|
|
62
62
|
export function useFirestoreDocData<T = unknown>(ref: DocumentReference<T>, options?: ReactFireOptions<T>): ObservableStatus<T> {
|
|
63
63
|
const idField = options ? checkIdField(options) : 'NO_ID_FIELD';
|
package/src/storage.tsx
CHANGED
|
@@ -33,7 +33,7 @@ export function useStorageDownloadURL<T = string>(ref: StorageReference, options
|
|
|
33
33
|
return useObservable(observableId, observable$, options);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
type StorageImageProps = {
|
|
36
|
+
export type StorageImageProps = {
|
|
37
37
|
storagePath: string;
|
|
38
38
|
storage?: FirebaseStorage;
|
|
39
39
|
suspense?: boolean;
|
|
@@ -49,7 +49,7 @@ function StorageFromContext(props: StorageImageProps & React.DetailedHTMLProps<R
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function INTERNALStorageImage(props: StorageImageProps & React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>): JSX.Element {
|
|
52
|
-
|
|
52
|
+
const { storage, storagePath, suspense, placeHolder, ...imgProps } = props;
|
|
53
53
|
|
|
54
54
|
const reactfireOptions: ReactFireOptions<string> = {
|
|
55
55
|
suspense: useSuspenseEnabledFromConfigAndContext(suspense),
|
|
@@ -76,7 +76,7 @@ function INTERNALStorageImage(props: StorageImageProps & React.DetailedHTMLProps
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export function StorageImage(props: StorageImageProps & React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>) {
|
|
79
|
-
|
|
79
|
+
const { storage } = props;
|
|
80
80
|
|
|
81
81
|
if (storage) {
|
|
82
82
|
return <INTERNALStorageImage {...props} />;
|
package/src/useObservable.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
|
2
3
|
import { Observable } from 'rxjs';
|
|
3
4
|
import { SuspenseSubject } from './SuspenseSubject';
|
|
4
5
|
import { useSuspenseEnabledFromConfigAndContext } from './firebaseApp';
|
|
@@ -16,17 +17,17 @@ if (!(globalThis as any as ReactFireGlobals)._reactFirePreloadedObservables) {
|
|
|
16
17
|
// Starts listening to an Observable.
|
|
17
18
|
// Call this once you know you're going to render a
|
|
18
19
|
// child that will consume the observable
|
|
19
|
-
export function preloadObservable<T>(source: Observable<T>, id: string) {
|
|
20
|
+
export function preloadObservable<T>(source: Observable<T>, id: string, suspenseEnabled = false) {
|
|
20
21
|
if (preloadedObservables.has(id)) {
|
|
21
22
|
return preloadedObservables.get(id) as SuspenseSubject<T>;
|
|
22
23
|
} else {
|
|
23
|
-
const observable = new SuspenseSubject(source, DEFAULT_TIMEOUT);
|
|
24
|
+
const observable = new SuspenseSubject(source, DEFAULT_TIMEOUT, suspenseEnabled);
|
|
24
25
|
preloadedObservables.set(id, observable);
|
|
25
26
|
return observable;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
interface ObservableStatusBase<T> {
|
|
30
31
|
/**
|
|
31
32
|
* The loading status.
|
|
32
33
|
*
|
|
@@ -52,7 +53,7 @@ export interface ObservableStatus<T> {
|
|
|
52
53
|
*
|
|
53
54
|
* 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`.
|
|
54
55
|
*/
|
|
55
|
-
data: T;
|
|
56
|
+
data: T | undefined;
|
|
56
57
|
/**
|
|
57
58
|
* Any error that may have occurred in the underlying observable
|
|
58
59
|
*/
|
|
@@ -63,75 +64,79 @@ export interface ObservableStatus<T> {
|
|
|
63
64
|
firstValuePromise: Promise<void>;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
newState.status = 'success';
|
|
82
|
-
return newState;
|
|
83
|
-
case 'error':
|
|
84
|
-
newState.status = 'error';
|
|
85
|
-
return newState;
|
|
86
|
-
case 'complete':
|
|
87
|
-
newState.isComplete = true;
|
|
88
|
-
return newState;
|
|
89
|
-
default:
|
|
90
|
-
throw new Error(`invalid action "${action}"`);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
67
|
+
export interface ObservableStatusSuccess<T> extends ObservableStatusBase<T> {
|
|
68
|
+
status: 'success';
|
|
69
|
+
data: T;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ObservableStatusError<T> extends ObservableStatusBase<T> {
|
|
73
|
+
status: 'error';
|
|
74
|
+
isComplete: true;
|
|
75
|
+
error: Error;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ObservableStatusLoading<T> extends ObservableStatusBase<T> {
|
|
79
|
+
status: 'loading';
|
|
80
|
+
data: undefined;
|
|
81
|
+
hasEmitted: false;
|
|
93
82
|
}
|
|
94
83
|
|
|
84
|
+
export type ObservableStatus<T> = ObservableStatusLoading<T> | ObservableStatusError<T> | ObservableStatusSuccess<T>;
|
|
85
|
+
|
|
95
86
|
export function useObservable<T = unknown>(observableId: string, source: Observable<T>, config: ReactFireOptions = {}): ObservableStatus<T> {
|
|
96
|
-
// Register the observable with the cache
|
|
97
87
|
if (!observableId) {
|
|
98
88
|
throw new Error('cannot call useObservable without an observableId');
|
|
99
89
|
}
|
|
100
|
-
|
|
90
|
+
|
|
91
|
+
const suspenseEnabled = useSuspenseEnabledFromConfigAndContext(config.suspense);
|
|
92
|
+
|
|
93
|
+
// Register the observable with the cache
|
|
94
|
+
const observable = preloadObservable(source, observableId, suspenseEnabled);
|
|
101
95
|
|
|
102
96
|
// Suspend if suspense is enabled and no initial data exists
|
|
103
97
|
const hasInitialData = config.hasOwnProperty('initialData') || config.hasOwnProperty('startWithValue');
|
|
104
98
|
const hasData = observable.hasValue || hasInitialData;
|
|
105
|
-
const suspenseEnabled = useSuspenseEnabledFromConfigAndContext(config.suspense);
|
|
106
99
|
if (suspenseEnabled === true && !hasData) {
|
|
107
100
|
throw observable.firstEmission;
|
|
108
101
|
}
|
|
109
102
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
return () => subscription.unsubscribe();
|
|
103
|
+
const subscribe = React.useCallback((onStoreChange: () => void) => {
|
|
104
|
+
const subscription = observable.subscribe({
|
|
105
|
+
next: () => {
|
|
106
|
+
onStoreChange();
|
|
107
|
+
},
|
|
108
|
+
error: (e) => {
|
|
109
|
+
onStoreChange();
|
|
110
|
+
throw e;
|
|
111
|
+
},
|
|
112
|
+
complete: () => {
|
|
113
|
+
onStoreChange();
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
subscription.unsubscribe();
|
|
119
|
+
}
|
|
120
|
+
}, [observable])
|
|
121
|
+
|
|
122
|
+
const getSnapshot = React.useCallback<() => ObservableStatus<T>>(() => {
|
|
123
|
+
return observable.immutableStatus;
|
|
134
124
|
}, [observable]);
|
|
135
125
|
|
|
136
|
-
|
|
126
|
+
const update = useSyncExternalStore(subscribe, getSnapshot);
|
|
127
|
+
|
|
128
|
+
// modify the value if initialData exists
|
|
129
|
+
if (!observable.hasValue && hasData) {
|
|
130
|
+
update.data = config?.initialData ?? config?.startWithValue;
|
|
131
|
+
update.status = 'success';
|
|
132
|
+
update.hasEmitted = true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// throw an error if there is an error
|
|
136
|
+
// TODO(jhuleatt) this is the current, tested-for, behavior. But do we actually want it?
|
|
137
|
+
if (update.error) {
|
|
138
|
+
throw update.error;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return update;
|
|
137
142
|
}
|