mirror of
https://github.com/zhigang1992/reactfire.git
synced 2026-01-12 22:51:28 +08:00
Using our subject/cache to fix the init race I introduced (#218)
* Using our subject/cache to fix the init race I introduced with the refactor * Naming * Await init on non-contextual app, if one is passed * void | Promise<any> better describes the contract * Global cache and test
This commit is contained in:
@@ -6,7 +6,7 @@ export * from './sdk';
|
||||
type FirebaseAppContextValue = firebase.app.App;
|
||||
|
||||
// INVESTIGATE I don't like magic strings, can we have export this in js-sdk?
|
||||
export const DEFAULT_APP_NAME = '[DEFAULT]';
|
||||
const DEFAULT_APP_NAME = '[DEFAULT]';
|
||||
|
||||
const FirebaseAppContext = React.createContext<
|
||||
FirebaseAppContextValue | undefined
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DEFAULT_APP_NAME } from '../index';
|
||||
import { useFirebaseApp } from '.';
|
||||
import * as firebase from 'firebase/app';
|
||||
import { Observable } from 'rxjs';
|
||||
import { preloadObservable } from '../useObservable';
|
||||
|
||||
type ComponentName =
|
||||
| 'analytics'
|
||||
@@ -62,17 +63,19 @@ function proxyComponent(
|
||||
componentName: ComponentName
|
||||
): FirebaseNamespaceComponent {
|
||||
let contextualApp: App | undefined;
|
||||
const useComponent = () => {
|
||||
const useComponent = (app?: App) => {
|
||||
contextualApp = useFirebaseApp();
|
||||
if (!firebase[componentName]) {
|
||||
throw importSDK(componentName);
|
||||
const sdkSubject = preload(componentName, app || contextualApp);
|
||||
if (!sdkSubject.hasValue) {
|
||||
throw sdkSubject.firstEmission;
|
||||
}
|
||||
sdkSubject.value; // get value to throw if there's an error
|
||||
return firebase[componentName];
|
||||
};
|
||||
return new Proxy(useComponent, {
|
||||
get: (target, p) => target()[p],
|
||||
apply: (target, _this, args) => {
|
||||
const component = target().bind(_this);
|
||||
const component = target(args[0]).bind(_this);
|
||||
// If they don't pass an app, assume the app in context rather than [DEFAULT]
|
||||
if (!args[0]) {
|
||||
args[0] = contextualApp;
|
||||
@@ -102,94 +105,100 @@ export const performance = usePerformance;
|
||||
export const remoteConfig = useRemoteConfig;
|
||||
export const storage = useStorage;
|
||||
|
||||
function preload(
|
||||
function preloadFactory(
|
||||
componentName: 'auth'
|
||||
): (
|
||||
firebaseApp?: App,
|
||||
settingsCallback?: (instanceFactory: App['auth']) => any
|
||||
settingsCallback?: (instanceFactory: App['auth']) => void | Promise<any>
|
||||
) => Promise<App['auth']>;
|
||||
function preload(
|
||||
function preloadFactory(
|
||||
componentName: 'analytics'
|
||||
): (
|
||||
firebaseApp?: App,
|
||||
settingsCallback?: (instanceFactory: App['analytics']) => any
|
||||
settingsCallback?: (instanceFactory: App['analytics']) => void | Promise<any>
|
||||
) => Promise<App['analytics']>;
|
||||
function preload(
|
||||
function preloadFactory(
|
||||
componentName: 'database'
|
||||
): (
|
||||
firebaseApp?: App,
|
||||
settingsCallback?: (instanceFactory: App['database']) => any
|
||||
settingsCallback?: (instanceFactory: App['database']) => void | Promise<any>
|
||||
) => Promise<App['database']>;
|
||||
function preload(
|
||||
function preloadFactory(
|
||||
componentName: 'firestore'
|
||||
): (
|
||||
firebaseApp?: App,
|
||||
settingsCallback?: (instanceFactory: App['firestore']) => any
|
||||
settingsCallback?: (instanceFactory: App['firestore']) => void | Promise<any>
|
||||
) => Promise<App['firestore']>;
|
||||
function preload(
|
||||
function preloadFactory(
|
||||
componentName: 'functions'
|
||||
): (
|
||||
firebaseApp?: App,
|
||||
settingsCallback?: (instanceFactory: App['functions']) => any
|
||||
settingsCallback?: (instanceFactory: App['functions']) => void | Promise<any>
|
||||
) => Promise<App['functions']>;
|
||||
function preload(
|
||||
function preloadFactory(
|
||||
componentName: 'messaging'
|
||||
): (
|
||||
firebaseApp?: App,
|
||||
settingsCallback?: (instanceFactory: App['messaging']) => any
|
||||
settingsCallback?: (instanceFactory: App['messaging']) => void | Promise<any>
|
||||
) => Promise<App['messaging']>;
|
||||
function preload(
|
||||
function preloadFactory(
|
||||
componentName: 'performance'
|
||||
): (
|
||||
firebaseApp?: App,
|
||||
settingsCallback?: (instanceFactory: App['performance']) => any
|
||||
settingsCallback?: (instanceFactory: App['performance']) => void | Promise<any>
|
||||
) => Promise<App['performance']>;
|
||||
function preload(
|
||||
function preloadFactory(
|
||||
componentName: 'remoteConfig'
|
||||
): (
|
||||
firebaseApp?: App,
|
||||
settingsCallback?: (instanceFactory: App['remoteConfig']) => any
|
||||
settingsCallback?: (instanceFactory: App['remoteConfig']) => void | Promise<any>
|
||||
) => Promise<App['remoteConfig']>;
|
||||
function preload(
|
||||
function preloadFactory(
|
||||
componentName: 'storage'
|
||||
): (
|
||||
firebaseApp?: App,
|
||||
settingsCallback?: (instanceFactory: App['storage']) => any
|
||||
settingsCallback?: (instanceFactory: App['storage']) => void | Promise<any>
|
||||
) => Promise<App['storage']>;
|
||||
function preload(componentName: ComponentName) {
|
||||
return async (
|
||||
function preloadFactory(componentName: ComponentName) {
|
||||
return (
|
||||
firebaseApp?: App,
|
||||
settingsCallback?: (instanceFactory: FirebaseInstanceFactory) => any
|
||||
) => {
|
||||
const app = firebaseApp || useFirebaseApp();
|
||||
const initialized = !!app[componentName];
|
||||
if (!initialized) {
|
||||
await importSDK(componentName);
|
||||
}
|
||||
const instanceFactory = app[componentName].bind(
|
||||
app
|
||||
) as FirebaseInstanceFactory;
|
||||
if (initialized) {
|
||||
if (settingsCallback) {
|
||||
console.warn(
|
||||
`${componentName} was already initialized on ${
|
||||
app.name == DEFAULT_APP_NAME ? 'the default app' : app.name
|
||||
}, ignoring settingsCallback`
|
||||
);
|
||||
}
|
||||
} else if (settingsCallback) {
|
||||
await Promise.resolve(settingsCallback(instanceFactory));
|
||||
}
|
||||
return instanceFactory;
|
||||
};
|
||||
) => preload(componentName, firebaseApp, settingsCallback).toPromise();
|
||||
}
|
||||
|
||||
export const preloadAuth = preload('auth');
|
||||
export const preloadAnalytics = preload('analytics');
|
||||
export const preloadDatabase = preload('database');
|
||||
export const preloadFirestore = preload('firestore');
|
||||
export const preloadFunctions = preload('functions');
|
||||
export const preloadMessaging = preload('messaging');
|
||||
export const preloadPerformance = preload('performance');
|
||||
export const preloadRemoteConfig = preload('remoteConfig');
|
||||
export const preloadStorage = preload('storage');
|
||||
function preload(
|
||||
componentName: ComponentName,
|
||||
firebaseApp?: App,
|
||||
settingsCallback: (instanceFactory: FirebaseInstanceFactory) => any = () => {}
|
||||
) {
|
||||
const app = firebaseApp || useFirebaseApp();
|
||||
return preloadObservable(
|
||||
new Observable(emitter => {
|
||||
importSDK(componentName)
|
||||
.then(() => {
|
||||
const instanceFactory = app[componentName].bind(
|
||||
app
|
||||
) as FirebaseInstanceFactory;
|
||||
Promise.resolve(settingsCallback(instanceFactory)).then(() => {
|
||||
emitter.next(instanceFactory);
|
||||
emitter.complete();
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
emitter.error(e);
|
||||
emitter.complete();
|
||||
});
|
||||
}),
|
||||
`firebase-sdk:${componentName}:${app.name}`
|
||||
);
|
||||
}
|
||||
|
||||
export const preloadAuth = preloadFactory('auth');
|
||||
export const preloadAnalytics = preloadFactory('analytics');
|
||||
export const preloadDatabase = preloadFactory('database');
|
||||
export const preloadFirestore = preloadFactory('firestore');
|
||||
export const preloadFunctions = preloadFactory('functions');
|
||||
export const preloadMessaging = preloadFactory('messaging');
|
||||
export const preloadPerformance = preloadFactory('performance');
|
||||
export const preloadRemoteConfig = preloadFactory('remoteConfig');
|
||||
export const preloadStorage = preloadFactory('storage');
|
||||
|
||||
@@ -10,9 +10,11 @@ import {
|
||||
useFirestoreCollectionData,
|
||||
useFirestoreDocData,
|
||||
useFirestoreDocDataOnce,
|
||||
useFirestoreDocOnce
|
||||
useFirestoreDocOnce,
|
||||
useFirestore
|
||||
} from '..';
|
||||
import { firestore } from 'firebase/app';
|
||||
import { preloadFirestore } from '../firebaseApp';
|
||||
|
||||
describe('Firestore', () => {
|
||||
let app: import('firebase').app.App;
|
||||
@@ -41,6 +43,48 @@ describe('Firestore', () => {
|
||||
.add({ a: 'hello' });
|
||||
});
|
||||
|
||||
describe('useFirestore', () => {
|
||||
|
||||
it('awaits the preloadFirestore setup', async () => {
|
||||
const app2 = firebase.initializeTestApp({
|
||||
projectId: '123456',
|
||||
databaseName: 'my-database',
|
||||
auth: { uid: 'alice' }
|
||||
});
|
||||
|
||||
let firestore: firebase.firestore.Firestore;
|
||||
let preloadResolved = false;
|
||||
let preloadResolve: (v?: unknown) => void;
|
||||
preloadFirestore(app2, () => new Promise(resolve => preloadResolve = resolve)).then(() => preloadResolved = true);
|
||||
|
||||
const Firestore = () => {
|
||||
const firestore = useFirestore(app2);
|
||||
return (
|
||||
<div data-testid="success"></div>
|
||||
);
|
||||
};
|
||||
const { getByTestId } = render(
|
||||
<FirebaseAppProvider firebase={app2}>
|
||||
<React.Suspense fallback={<h1 data-testid="fallback">Fallback</h1>}>
|
||||
<Firestore />
|
||||
</React.Suspense>
|
||||
</FirebaseAppProvider>
|
||||
);
|
||||
|
||||
await waitForElement(() => getByTestId('fallback'));
|
||||
expect(preloadResolved).toEqual(false);
|
||||
|
||||
await waitForElement(() => getByTestId('success')).then(() => fail('expected throw')).catch(() => {});
|
||||
expect(preloadResolved).toEqual(false);
|
||||
|
||||
preloadResolve();
|
||||
|
||||
await waitForElement(() => getByTestId('success'));
|
||||
expect(preloadResolved).toEqual(true);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('useFirestoreDoc', () => {
|
||||
it('can get a Firestore document [TEST REQUIRES EMULATOR]', async () => {
|
||||
const mockData = { a: 'hello' };
|
||||
|
||||
@@ -2,8 +2,22 @@ import * as React from 'react';
|
||||
import { Observable } from 'rxjs';
|
||||
import { SuspenseSubject } from './SuspenseSubject';
|
||||
|
||||
const globalThis = function() {
|
||||
if (typeof self !== 'undefined') { return self; }
|
||||
if (typeof window !== 'undefined') { return window; }
|
||||
if (typeof global !== 'undefined') { return global; }
|
||||
throw new Error('unable to locate global object');
|
||||
}();
|
||||
|
||||
const PRELOADED_OBSERVABLES = '_reactFirePreloadedObservables';
|
||||
const DEFAULT_TIMEOUT = 30_000;
|
||||
const preloadedObservables = new Map<string, SuspenseSubject<unknown>>();
|
||||
|
||||
// Since we're side-effect free, we need to ensure our observable cache is global
|
||||
const preloadedObservables = globalThis[PRELOADED_OBSERVABLES] || new Map<string, SuspenseSubject<unknown>>();
|
||||
|
||||
if (!globalThis[PRELOADED_OBSERVABLES]) {
|
||||
globalThis[PRELOADED_OBSERVABLES] = preloadedObservables;
|
||||
}
|
||||
|
||||
// Starts listening to an Observable.
|
||||
// Call this once you know you're going to render a
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"dependencies": {
|
||||
"@types/jest": "^24.9.0",
|
||||
"firebase": "^7.9.1",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
"react": "0.0.0-experimental-5de5b6150",
|
||||
"react-dom": "0.0.0-experimental-5de5b6150",
|
||||
"react-firebaseui": "^4.0.0",
|
||||
|
||||
@@ -7723,6 +7723,11 @@ join-path@^1.1.1:
|
||||
url-join "0.0.1"
|
||||
valid-url "^1"
|
||||
|
||||
js-levenshtein@^1.1.6:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
|
||||
integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
|
||||
Reference in New Issue
Block a user