reactfire 4.2.3 → 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 +7 -1
- package/dist/auth.d.ts +4 -4
- package/dist/firebaseApp.d.ts +2 -3
- package/dist/firestore.d.ts +5 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2204 -2286
- 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 +11 -11
- package/dist/storage.d.ts +3 -4
- package/dist/useObservable.d.ts +19 -3
- package/package.json +36 -35
- package/src/SuspenseSubject.ts +48 -3
- package/src/auth.tsx +22 -5
- package/src/firebaseApp.tsx +4 -2
- package/src/firestore.tsx +9 -9
- package/src/performance.tsx +7 -6
- package/src/sdk.tsx +1 -1
- package/src/storage.tsx +5 -5
- package/src/useObservable.ts +76 -58
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,92 @@ 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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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;
|
|
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
|
+
// 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;
|
|
137
155
|
}
|