reactfire 4.2.3-exp.c7357da → 4.2.4
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/README.md +4 -0
- package/dist/SuspenseSubject.d.ts +5 -0
- package/dist/auth.d.ts +1 -1
- package/dist/firebaseApp.d.ts +1 -2
- package/dist/firestore.d.ts +3 -3
- package/dist/index.js +2204 -2214
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +4 -122
- package/dist/index.umd.cjs.map +1 -1
- package/dist/performance.d.ts +1 -1
- package/dist/sdk.d.ts +9 -9
- package/dist/storage.d.ts +2 -3
- package/dist/useObservable.d.ts +18 -2
- package/package.json +17 -15
- package/src/SuspenseSubject.ts +41 -1
- package/src/auth.tsx +20 -3
- package/src/firebaseApp.tsx +3 -1
- package/src/firestore.tsx +4 -4
- package/src/performance.tsx +7 -6
- package/src/sdk.tsx +1 -1
- package/src/storage.tsx +3 -3
- package/src/useObservable.ts +71 -54
package/dist/performance.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export interface SuspensePerfProps {
|
|
|
4
4
|
traceId: string;
|
|
5
5
|
fallback: React.ReactNode;
|
|
6
6
|
}
|
|
7
|
-
export declare function SuspenseWithPerf({ children, traceId, fallback }: SuspensePerfProps):
|
|
7
|
+
export declare function SuspenseWithPerf({ children, traceId, fallback }: SuspensePerfProps): React.ReactElement;
|
package/dist/sdk.d.ts
CHANGED
|
@@ -23,31 +23,31 @@ export declare const RemoteConfigSdkContext: React.Context<RemoteConfig | undefi
|
|
|
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
|
-
}>) => React.
|
|
26
|
+
}>) => React.ReactElement;
|
|
27
27
|
export declare const AuthProvider: (props: React.PropsWithChildren<{
|
|
28
28
|
sdk: Auth;
|
|
29
|
-
}>) => React.
|
|
29
|
+
}>) => React.ReactElement;
|
|
30
30
|
export declare const AnalyticsProvider: (props: React.PropsWithChildren<{
|
|
31
31
|
sdk: Analytics;
|
|
32
|
-
}>) => React.
|
|
32
|
+
}>) => React.ReactElement;
|
|
33
33
|
export declare const DatabaseProvider: (props: React.PropsWithChildren<{
|
|
34
34
|
sdk: Database;
|
|
35
|
-
}>) => React.
|
|
35
|
+
}>) => React.ReactElement;
|
|
36
36
|
export declare const FirestoreProvider: (props: React.PropsWithChildren<{
|
|
37
37
|
sdk: Firestore;
|
|
38
|
-
}>) => React.
|
|
38
|
+
}>) => React.ReactElement;
|
|
39
39
|
export declare const FunctionsProvider: (props: React.PropsWithChildren<{
|
|
40
40
|
sdk: Functions;
|
|
41
|
-
}>) => React.
|
|
41
|
+
}>) => React.ReactElement;
|
|
42
42
|
export declare const PerformanceProvider: (props: React.PropsWithChildren<{
|
|
43
43
|
sdk: FirebasePerformance;
|
|
44
|
-
}>) => React.
|
|
44
|
+
}>) => React.ReactElement;
|
|
45
45
|
export declare const StorageProvider: (props: React.PropsWithChildren<{
|
|
46
46
|
sdk: FirebaseStorage;
|
|
47
|
-
}>) => React.
|
|
47
|
+
}>) => React.ReactElement;
|
|
48
48
|
export declare const RemoteConfigProvider: (props: React.PropsWithChildren<{
|
|
49
49
|
sdk: RemoteConfig;
|
|
50
|
-
}>) => React.
|
|
50
|
+
}>) => React.ReactElement;
|
|
51
51
|
export declare const useAppCheck: () => AppCheck;
|
|
52
52
|
export declare const useAuth: () => Auth;
|
|
53
53
|
export declare const useAnalytics: () => 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
|
-
type StorageImageProps = {
|
|
19
|
+
export type StorageImageProps = {
|
|
20
20
|
storagePath: string;
|
|
21
21
|
storage?: FirebaseStorage;
|
|
22
22
|
suspense?: boolean;
|
|
23
|
-
placeHolder?:
|
|
23
|
+
placeHolder?: React.ReactNode;
|
|
24
24
|
};
|
|
25
25
|
export declare function StorageImage(props: StorageImageProps & React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>): React.JSX.Element;
|
|
26
|
-
export {};
|
package/dist/useObservable.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Observable } from 'rxjs';
|
|
|
2
2
|
import { SuspenseSubject } from './SuspenseSubject';
|
|
3
3
|
import { ReactFireOptions } from './';
|
|
4
4
|
export declare function preloadObservable<T>(source: Observable<T>, id: string, suspenseEnabled?: boolean): SuspenseSubject<T>;
|
|
5
|
-
|
|
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.
|
|
2
|
+
"version": "4.2.4",
|
|
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 || next",
|
|
39
|
+
"firebase": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || next",
|
|
40
40
|
"react": ">=16 || experimental"
|
|
41
41
|
},
|
|
42
42
|
"husky": {
|
|
@@ -71,42 +71,44 @@
|
|
|
71
71
|
}
|
|
72
72
|
],
|
|
73
73
|
"devDependencies": {
|
|
74
|
-
"@rollup/plugin-typescript": "^
|
|
74
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
75
75
|
"@size-limit/preset-small-lib": "^8.2.6",
|
|
76
76
|
"@testing-library/jest-dom": "^5.16.5",
|
|
77
77
|
"@testing-library/react": "^14.0.0",
|
|
78
78
|
"@types/react": "^18.2.0",
|
|
79
79
|
"@types/react-dom": "^18.2.0",
|
|
80
|
+
"@types/use-sync-external-store": "^0.0.3",
|
|
80
81
|
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
|
81
82
|
"@typescript-eslint/parser": "^5.60.1",
|
|
82
|
-
"@vitejs/plugin-react": "^
|
|
83
|
-
"@vitest/ui": "^
|
|
83
|
+
"@vitejs/plugin-react": "^6.0.2",
|
|
84
|
+
"@vitest/ui": "^4.1.9",
|
|
84
85
|
"cross-fetch": "^3.1.6",
|
|
85
86
|
"eslint": "^8.43.0",
|
|
86
87
|
"eslint-plugin-no-only-tests": "^3.1.0",
|
|
87
88
|
"eslint-plugin-react": "^7.32.2",
|
|
88
89
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
89
|
-
"firebase": "^
|
|
90
|
-
"firebase-tools": "^
|
|
90
|
+
"firebase": "^11.10.0",
|
|
91
|
+
"firebase-tools": "^15.22.3",
|
|
91
92
|
"globalthis": "^1.0.3",
|
|
92
93
|
"husky": "^8.0.3",
|
|
93
94
|
"jest-environment-jsdom": "^29.5.0",
|
|
94
95
|
"jsdom": "^22.1.0",
|
|
95
96
|
"markdown-toc": "^1.2.0",
|
|
96
|
-
"prettier": "^
|
|
97
|
+
"prettier": "^3.8.4",
|
|
97
98
|
"react": "^18.2.0",
|
|
98
99
|
"react-dom": "^18.2.0",
|
|
99
100
|
"react-test-renderer": "^18.2.0",
|
|
100
101
|
"rollup-plugin-visualizer": "^5.9.2",
|
|
101
102
|
"size-limit": "^8.2.6",
|
|
102
|
-
"typedoc": "^0.
|
|
103
|
-
"typedoc-plugin-markdown": "^
|
|
104
|
-
"typescript": "^
|
|
105
|
-
"vite": "^
|
|
106
|
-
"vitest": "^
|
|
103
|
+
"typedoc": "^0.28.19",
|
|
104
|
+
"typedoc-plugin-markdown": "^4.12.0",
|
|
105
|
+
"typescript": "^6.0.3",
|
|
106
|
+
"vite": "^8.0.16",
|
|
107
|
+
"vitest": "^4.1.9"
|
|
107
108
|
},
|
|
108
109
|
"dependencies": {
|
|
109
|
-
"rxfire": "^6.0
|
|
110
|
-
"rxjs": "^6.6.3 || ^7.0.1"
|
|
110
|
+
"rxfire": "^6.1.0",
|
|
111
|
+
"rxjs": "^6.6.3 || ^7.0.1",
|
|
112
|
+
"use-sync-external-store": "^1.2.0"
|
|
111
113
|
}
|
|
112
114
|
}
|
package/src/SuspenseSubject.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
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;
|
|
6
7
|
private _hasValue = false;
|
|
7
|
-
private _timeoutHandler:
|
|
8
|
+
private _timeoutHandler: ReturnType<typeof setTimeout>;
|
|
8
9
|
private _firstEmission: Promise<void>;
|
|
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;
|
|
@@ -18,6 +21,16 @@ export class SuspenseSubject<T> extends Subject<T> {
|
|
|
18
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)
|
|
@@ -69,10 +87,27 @@ export class SuspenseSubject<T> extends Subject<T> {
|
|
|
69
87
|
return this._firstEmission;
|
|
70
88
|
}
|
|
71
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
|
+
|
|
72
106
|
private _next(value: T) {
|
|
73
107
|
this._hasValue = true;
|
|
74
108
|
this._value = value;
|
|
75
109
|
this._resolveFirstEmission();
|
|
110
|
+
this._updateImmutableStatus();
|
|
76
111
|
}
|
|
77
112
|
|
|
78
113
|
private _reset() {
|
|
@@ -84,6 +119,7 @@ export class SuspenseSubject<T> extends Subject<T> {
|
|
|
84
119
|
this._value = undefined;
|
|
85
120
|
this._error = undefined;
|
|
86
121
|
this._firstEmission = new Promise<void>((resolve) => (this._resolveFirstEmission = resolve));
|
|
122
|
+
this._updateImmutableStatus();
|
|
87
123
|
}
|
|
88
124
|
|
|
89
125
|
_subscribe(subscriber: Subscriber<T>): Subscription {
|
|
@@ -97,4 +133,8 @@ export class SuspenseSubject<T> extends Subject<T> {
|
|
|
97
133
|
get ourError() {
|
|
98
134
|
return this._error;
|
|
99
135
|
}
|
|
136
|
+
|
|
137
|
+
get immutableStatus() {
|
|
138
|
+
return this._immutableStatus;
|
|
139
|
+
}
|
|
100
140
|
}
|
package/src/auth.tsx
CHANGED
|
@@ -25,7 +25,17 @@ export function useUser<T = unknown>(options?: ReactFireOptions<T>): ObservableS
|
|
|
25
25
|
const observableId = `auth:user:${auth.name}`;
|
|
26
26
|
const observable$ = user(auth);
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
const _options: ReactFireOptions<T> = { ...options };
|
|
29
|
+
|
|
30
|
+
// If a user is already signed in, seed initialData so consumers see the user
|
|
31
|
+
// synchronously on the first render without waiting for the async observable.
|
|
32
|
+
// We only do this when currentUser is truthy to avoid masking the uninitialized
|
|
33
|
+
// (null before auth has loaded from storage) case as "signed out".
|
|
34
|
+
if (auth.currentUser && !('initialData' in _options) && !('startWithValue' in _options)) {
|
|
35
|
+
_options.initialData = auth.currentUser as unknown as T;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return useObservable(observableId, observable$, _options);
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
export function useIdTokenResult(user: User, forceRefresh = false, options?: ReactFireOptions<IdTokenResult>): ObservableStatus<IdTokenResult> {
|
|
@@ -201,7 +211,14 @@ function getClaimsObjectValidator(requiredClaims: Claims): ClaimsValidator {
|
|
|
201
211
|
* Meant for Concurrent mode only (`<FirebaseAppProvider suspense=true />`). [More detail](https://github.com/FirebaseExtended/reactfire/issues/325#issuecomment-827654376).
|
|
202
212
|
*/
|
|
203
213
|
export function ClaimsCheck({ user, fallback, children, requiredClaims }: ClaimsCheckProps) {
|
|
204
|
-
const { data } = useIdTokenResult(user, false);
|
|
214
|
+
const { data, status, error } = useIdTokenResult(user, false);
|
|
215
|
+
|
|
216
|
+
if (status === 'loading') {
|
|
217
|
+
throw new Error('ClaimsCheck must be run in Suspense mode');
|
|
218
|
+
} else if (status === 'error') {
|
|
219
|
+
throw error
|
|
220
|
+
}
|
|
221
|
+
|
|
205
222
|
const { claims } = data;
|
|
206
223
|
const missingClaims: { [key: string]: { expected: string; actual: string | undefined } } = {};
|
|
207
224
|
|
|
@@ -237,7 +254,7 @@ export function ClaimsCheck({ user, fallback, children, requiredClaims }: Claims
|
|
|
237
254
|
*
|
|
238
255
|
* Meant for Concurrent mode only (`<FirebaseAppProvider suspense=true />`). [More detail](https://github.com/FirebaseExtended/reactfire/issues/325#issuecomment-827654376).
|
|
239
256
|
*/
|
|
240
|
-
export function AuthCheck({ fallback, children, requiredClaims }: AuthCheckProps):
|
|
257
|
+
export function AuthCheck({ fallback, children, requiredClaims }: AuthCheckProps): React.ReactElement {
|
|
241
258
|
const { data: user } = useUser<User>();
|
|
242
259
|
|
|
243
260
|
const suspenseMode = useSuspenseEnabledFromConfigAndContext();
|
package/src/firebaseApp.tsx
CHANGED
|
@@ -3,13 +3,15 @@ import { getApps, initializeApp, registerVersion } from 'firebase/app';
|
|
|
3
3
|
|
|
4
4
|
import type { FirebaseApp, FirebaseOptions } from 'firebase/app';
|
|
5
5
|
|
|
6
|
+
declare const process: { env: Record<string, string | undefined> };
|
|
7
|
+
|
|
6
8
|
// INVESTIGATE I don't like magic strings, can we have export this in js-sdk?
|
|
7
9
|
const DEFAULT_APP_NAME = '[DEFAULT]';
|
|
8
10
|
|
|
9
11
|
const FirebaseAppContext = React.createContext<FirebaseApp | undefined>(undefined);
|
|
10
12
|
const SuspenseEnabledContext = React.createContext<boolean>(false);
|
|
11
13
|
|
|
12
|
-
interface FirebaseAppProviderProps {
|
|
14
|
+
export interface FirebaseAppProviderProps {
|
|
13
15
|
firebaseApp?: FirebaseApp;
|
|
14
16
|
firebaseConfig?: FirebaseOptions;
|
|
15
17
|
appName?: string;
|
package/src/firestore.tsx
CHANGED
|
@@ -59,25 +59,25 @@ export function useFirestoreDocOnce<T = DocumentData>(ref: DocumentReference<T>,
|
|
|
59
59
|
/**
|
|
60
60
|
* Subscribe to Firestore Document changes and unwrap the document into a plain object
|
|
61
61
|
*/
|
|
62
|
-
export function useFirestoreDocData<T = unknown>(ref: DocumentReference<T>, options?: ReactFireOptions<T>): ObservableStatus<T> {
|
|
62
|
+
export function useFirestoreDocData<T = unknown>(ref: DocumentReference<T>, options?: ReactFireOptions<T>): ObservableStatus<T | undefined> {
|
|
63
63
|
const idField = options ? checkIdField(options) : 'NO_ID_FIELD';
|
|
64
64
|
|
|
65
65
|
const observableId = `firestore:docData:${ref.firestore.app.name}:${ref.path}:idField=${idField}`;
|
|
66
66
|
const observable = docData(ref, { idField });
|
|
67
67
|
|
|
68
|
-
return useObservable(observableId, observable, options)
|
|
68
|
+
return useObservable(observableId, observable, options) as ObservableStatus<T>;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
72
|
* Get a Firestore document, unwrap the document into a plain object, and don't subscribe to changes
|
|
73
73
|
*/
|
|
74
|
-
export function useFirestoreDocDataOnce<T = unknown>(ref: DocumentReference<T>, options?: ReactFireOptions<T>): ObservableStatus<T> {
|
|
74
|
+
export function useFirestoreDocDataOnce<T = unknown>(ref: DocumentReference<T>, options?: ReactFireOptions<T>): ObservableStatus<T | undefined> {
|
|
75
75
|
const idField = options ? checkIdField(options) : 'NO_ID_FIELD';
|
|
76
76
|
|
|
77
77
|
const observableId = `firestore:docDataOnce:${ref.firestore.app.name}:${ref.path}:idField=${idField}`;
|
|
78
78
|
const observable$ = docData(ref, { idField }).pipe(first());
|
|
79
79
|
|
|
80
|
-
return useObservable(observableId, observable$, options)
|
|
80
|
+
return useObservable(observableId, observable$, options) as ObservableStatus<T>;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
package/src/performance.tsx
CHANGED
|
@@ -6,20 +6,21 @@ export interface SuspensePerfProps {
|
|
|
6
6
|
fallback: React.ReactNode;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export function SuspenseWithPerf({ children, traceId, fallback }: SuspensePerfProps):
|
|
9
|
+
export function SuspenseWithPerf({ children, traceId, fallback }: SuspensePerfProps): React.ReactElement {
|
|
10
10
|
// TODO: Should this import firebase/performance?
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const perf = typeof globalThis !== 'undefined' ? globalThis.performance : undefined;
|
|
13
|
+
const entries = perf?.getEntriesByName?.(traceId, 'measure') || [];
|
|
13
14
|
const startMarkName = `_${traceId}Start[${entries.length}]`;
|
|
14
15
|
const endMarkName = `_${traceId}End[${entries.length}]`;
|
|
15
16
|
|
|
16
17
|
const Fallback = () => {
|
|
17
|
-
React.
|
|
18
|
-
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
perf?.mark?.(startMarkName);
|
|
19
20
|
|
|
20
21
|
return () => {
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
perf?.mark?.(endMarkName);
|
|
23
|
+
perf?.measure?.(traceId, startMarkName, endMarkName);
|
|
23
24
|
};
|
|
24
25
|
}, []);
|
|
25
26
|
|
package/src/sdk.tsx
CHANGED
|
@@ -28,7 +28,7 @@ export const RemoteConfigSdkContext = React.createContext<RemoteConfig | undefin
|
|
|
28
28
|
type FirebaseSdks = Analytics | AppCheck | Auth | Database | Firestore | FirebasePerformance | FirebaseStorage | Functions | RemoteConfig;
|
|
29
29
|
|
|
30
30
|
function getSdkProvider<Sdk extends FirebaseSdks>(SdkContext: React.Context<Sdk | undefined>) {
|
|
31
|
-
return function SdkProvider(props: React.PropsWithChildren<{ sdk: Sdk }>) {
|
|
31
|
+
return function SdkProvider(props: React.PropsWithChildren<{ sdk: Sdk }>): React.ReactElement {
|
|
32
32
|
if (!props.sdk) throw new Error('no sdk provided');
|
|
33
33
|
|
|
34
34
|
const contextualAppName = useFirebaseApp().name;
|
package/src/storage.tsx
CHANGED
|
@@ -33,11 +33,11 @@ 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;
|
|
40
|
-
placeHolder?:
|
|
40
|
+
placeHolder?: React.ReactNode;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
function StorageFromContext(props: StorageImageProps & React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>) {
|
|
@@ -48,7 +48,7 @@ function StorageFromContext(props: StorageImageProps & React.DetailedHTMLProps<R
|
|
|
48
48
|
return <INTERNALStorageImage {...props} />;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function INTERNALStorageImage(props: StorageImageProps & React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>):
|
|
51
|
+
function INTERNALStorageImage(props: StorageImageProps & React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>): React.ReactNode {
|
|
52
52
|
const { storage, storagePath, suspense, placeHolder, ...imgProps } = props;
|
|
53
53
|
|
|
54
54
|
const reactfireOptions: ReactFireOptions<string> = {
|
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';
|
|
@@ -26,7 +27,7 @@ export function preloadObservable<T>(source: Observable<T>, id: string, suspense
|
|
|
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,42 +64,33 @@ export interface ObservableStatus<T> {
|
|
|
63
64
|
firstValuePromise: Promise<void>;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
...state,
|
|
71
|
-
hasEmitted: state.hasEmitted || observable.hasValue,
|
|
72
|
-
error: observable.ourError,
|
|
73
|
-
firstValuePromise: observable.firstEmission,
|
|
74
|
-
};
|
|
75
|
-
if (observable.hasValue) {
|
|
76
|
-
newState.data = observable.value;
|
|
77
|
-
}
|
|
67
|
+
export interface ObservableStatusSuccess<T> extends ObservableStatusBase<T> {
|
|
68
|
+
status: 'success';
|
|
69
|
+
data: T;
|
|
70
|
+
}
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
};
|
|
72
|
+
export interface ObservableStatusError<T> extends ObservableStatusBase<T> {
|
|
73
|
+
status: 'error';
|
|
74
|
+
isComplete: true;
|
|
75
|
+
error: Error;
|
|
93
76
|
}
|
|
94
77
|
|
|
78
|
+
export interface ObservableStatusLoading<T> extends ObservableStatusBase<T> {
|
|
79
|
+
status: 'loading';
|
|
80
|
+
data: undefined;
|
|
81
|
+
hasEmitted: false;
|
|
82
|
+
}
|
|
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
|
}
|
|
90
|
+
|
|
100
91
|
const suspenseEnabled = useSuspenseEnabledFromConfigAndContext(config.suspense);
|
|
101
92
|
|
|
93
|
+
// Register the observable with the cache
|
|
102
94
|
const observable = preloadObservable(source, observableId, suspenseEnabled);
|
|
103
95
|
|
|
104
96
|
// Suspend if suspense is enabled and no initial data exists
|
|
@@ -108,31 +100,56 @@ export function useObservable<T = unknown>(observableId: string, source: Observa
|
|
|
108
100
|
throw observable.firstEmission;
|
|
109
101
|
}
|
|
110
102
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
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;
|
|
135
124
|
}, [observable]);
|
|
136
125
|
|
|
137
|
-
|
|
126
|
+
const update = useSyncExternalStore(subscribe, getSnapshot);
|
|
127
|
+
|
|
128
|
+
// Return a new object with initialData overlaid rather than mutating the shared
|
|
129
|
+
// _immutableStatus reference, which is the same object across all components
|
|
130
|
+
// using the same observableId.
|
|
131
|
+
if (!observable.hasValue && hasData) {
|
|
132
|
+
const initialDataValue = config?.initialData ?? config?.startWithValue;
|
|
133
|
+
|
|
134
|
+
// In suspense mode, throw errors so React Error Boundaries can catch them.
|
|
135
|
+
// In non-suspense mode, surface errors via status so consumers can handle them locally.
|
|
136
|
+
if (suspenseEnabled && update.error) {
|
|
137
|
+
throw update.error;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
...update,
|
|
142
|
+
data: initialDataValue,
|
|
143
|
+
status: 'success',
|
|
144
|
+
hasEmitted: true,
|
|
145
|
+
} as ObservableStatus<T>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// throw an error if there is an error
|
|
149
|
+
// TODO(jhuleatt) this is the current, tested-for, behavior. But do we actually want it?
|
|
150
|
+
if (update.error) {
|
|
151
|
+
throw update.error;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return update;
|
|
138
155
|
}
|