diff --git a/README.md b/README.md index c3e61b17..edc6e874 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ npm install --save reactfire firebase yarn add reactfire firebase ``` +Depending on your targeted platforms you may need to install polyfills. The most commonly needed will be [globalThis](https://caniuse.com/#search=globalThis) and [Proxy](https://caniuse.com/#search=Proxy). + - [**Quickstart**](./docs/quickstart.md) - [**Common Use Cases**](./docs/use.md) - [**API Reference**](./docs/reference.md) @@ -40,12 +42,7 @@ Check out the ```jsx import React, { Component } from 'react'; import { createRoot } from 'react-dom'; -import { - FirebaseAppProvider, - useFirestoreDocData, - useFirestore, - SuspenseWithPerf -} from 'reactfire'; +import { FirebaseAppProvider, useFirestoreDocData, useFirestore, SuspenseWithPerf } from 'reactfire'; const firebaseConfig = { /* Add your config from the Firebase Console */ @@ -70,10 +67,7 @@ function App() { return (

🌯

- loading burrito status...

} - traceId={'load-burrito-status'} - > + loading burrito status...

} traceId={'load-burrito-status'}>
diff --git a/reactfire/database/index.tsx b/reactfire/database/index.tsx index 77e43b5d..fd273d2f 100644 --- a/reactfire/database/index.tsx +++ b/reactfire/database/index.tsx @@ -1,30 +1,35 @@ import { database } from 'firebase/app'; import { list, object, QueryChange, listVal } from 'rxfire/database'; -import { - ReactFireOptions, - useObservable, - checkIdField, - checkStartWithValue -} from '..'; +import { ReactFireOptions, useObservable, checkIdField, checkStartWithValue } from '..'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; +const CACHED_QUERIES = '_reactFireDatabaseCachedQueries'; + +// Since we're side-effect free, we need to ensure our observableId cache is global +const cachedQueries: Array = globalThis[CACHED_QUERIES] || []; + +if (!globalThis[CACHED_QUERIES]) { + globalThis[CACHED_QUERIES] = cachedQueries; +} + +function getUniqueIdForDatabaseQuery(query: database.Query) { + const index = cachedQueries.findIndex(cachedQuery => cachedQuery.isEqual(query)); + if (index > -1) { + return index; + } + return cachedQueries.push(query) - 1; +} + /** * Subscribe to a Realtime Database object * * @param ref - Reference to the DB object you want to listen to * @param options */ -export function useDatabaseObject( - ref: database.Reference, - options?: ReactFireOptions -): QueryChange | T { - return useObservable( - object(ref), - `database:object:${ref.toString()}`, - options ? options.startWithValue : undefined - ); +export function useDatabaseObject(ref: database.Reference, options?: ReactFireOptions): QueryChange | T { + return useObservable(object(ref), `database:object:${ref.toString()}`, options ? options.startWithValue : undefined); } // ============================================================================ @@ -50,23 +55,9 @@ function changeToData(change: QueryChange, keyField?: string): {} { } // ============================================================================ -export function useDatabaseObjectData( - ref: database.Reference, - options?: ReactFireOptions -): T { +export function useDatabaseObjectData(ref: database.Reference, options?: ReactFireOptions): T { const idField = checkIdField(options); - return useObservable( - objectVal(ref, idField), - `database:objectVal:${ref.toString()}:idField=${idField}`, - checkStartWithValue(options) - ); -} - -// Realtime Database has an undocumented method -// that helps us build a unique ID for the query -// https://github.com/firebase/firebase-js-sdk/blob/aca99669dd8ed096f189578c47a56a8644ac62e6/packages/database/src/api/Query.ts#L601 -interface _QueryWithId extends database.Query { - queryIdentifier(): string; + return useObservable(objectVal(ref, idField), `database:objectVal:${ref.toString()}:idField=${idField}`, checkStartWithValue(options)); } /** @@ -79,23 +70,12 @@ export function useDatabaseList( ref: database.Reference | database.Query, options?: ReactFireOptions ): QueryChange[] | T[] { - const hash = `database:list:${ref.toString()}|${(ref as _QueryWithId).queryIdentifier()}`; + const hash = `database:list:${getUniqueIdForDatabaseQuery(ref)}`; - return useObservable( - list(ref), - hash, - options ? options.startWithValue : undefined - ); + return useObservable(list(ref), hash, options ? options.startWithValue : undefined); } -export function useDatabaseListData( - ref: database.Reference | database.Query, - options?: ReactFireOptions -): T[] { +export function useDatabaseListData(ref: database.Reference | database.Query, options?: ReactFireOptions): T[] { const idField = checkIdField(options); - return useObservable( - listVal(ref, idField), - `database:listVal:${ref.toString()}|${(ref as _QueryWithId).queryIdentifier()}:idField=${idField}`, - checkStartWithValue(options) - ); + return useObservable(listVal(ref, idField), `database:listVal:${getUniqueIdForDatabaseQuery(ref)}:idField=${idField}`, checkStartWithValue(options)); } diff --git a/reactfire/firestore/index.tsx b/reactfire/firestore/index.tsx index c5d0d8a8..62f32462 100644 --- a/reactfire/firestore/index.tsx +++ b/reactfire/firestore/index.tsx @@ -1,21 +1,27 @@ import { firestore } from 'firebase/app'; -import { - collectionData, - doc, - docData, - fromCollectionRef -} from 'rxfire/firestore'; -import { - preloadFirestore, - ReactFireOptions, - useObservable, - checkIdField, - checkStartWithValue -} from '..'; +import { collectionData, doc, docData, fromCollectionRef } from 'rxfire/firestore'; +import { preloadFirestore, ReactFireOptions, useObservable, checkIdField, checkStartWithValue } from '..'; import { preloadObservable } from '../useObservable'; import { first } from 'rxjs/operators'; import { useFirebaseApp } from '../firebaseApp'; +const CACHED_QUERIES = '_reactFireFirestoreQueryCache'; + +// Since we're side-effect free, we need to ensure our observableId cache is global +const cachedQueries: Array = globalThis[CACHED_QUERIES] || []; + +if (!globalThis[CACHED_QUERIES]) { + globalThis[CACHED_QUERIES] = cachedQueries; +} + +function getUniqueIdForFirestoreQuery(query: firestore.Query) { + const index = cachedQueries.findIndex(cachedQuery => cachedQuery.isEqual(query)); + if (index > -1) { + return index; + } + return cachedQueries.push(query) - 1; +} + // starts a request for a firestore doc. // imports the firestore SDK automatically // if it hasn't been imported yet. @@ -23,18 +29,13 @@ import { useFirebaseApp } from '../firebaseApp'; // there's a decent chance this gets called before the Firestore SDK // has been imported, so it takes a refProvider instead of a ref export function preloadFirestoreDoc( - refProvider: ( - firestore: firebase.firestore.Firestore - ) => firestore.DocumentReference, + refProvider: (firestore: firebase.firestore.Firestore) => firestore.DocumentReference, options?: { firebaseApp?: firebase.app.App } ) { const firebaseApp = options?.firebaseApp || useFirebaseApp(); - return preloadFirestore({firebaseApp}).then(firestore => { + return preloadFirestore({ firebaseApp }).then(firestore => { const ref = refProvider(firestore()); - return preloadObservable( - doc(ref), - `firestore:doc:${firebaseApp.name}:${ref.path}` - ); + return preloadObservable(doc(ref), `firestore:doc:${firebaseApp.name}:${ref.path}`); }); } @@ -44,15 +45,8 @@ export function preloadFirestoreDoc( * @param ref - Reference to the document you want to listen to * @param options */ -export function useFirestoreDoc( - ref: firestore.DocumentReference, - options?: ReactFireOptions -): T extends {} ? T : firestore.DocumentSnapshot { - return useObservable( - doc(ref), - `firestore:doc:${ref.firestore.app.name}:${ref.path}`, - options ? options.startWithValue : undefined - ); +export function useFirestoreDoc(ref: firestore.DocumentReference, options?: ReactFireOptions): T extends {} ? T : firestore.DocumentSnapshot { + return useObservable(doc(ref), `firestore:doc:${ref.firestore.app.name}:${ref.path}`, options ? options.startWithValue : undefined); } /** @@ -65,11 +59,7 @@ export function useFirestoreDocOnce( ref: firestore.DocumentReference, options?: ReactFireOptions ): T extends {} ? T : firestore.DocumentSnapshot { - return useObservable( - doc(ref).pipe(first()), - `firestore:docOnce:${ref.firestore.app.name}:${ref.path}`, - checkStartWithValue(options) - ); + return useObservable(doc(ref).pipe(first()), `firestore:docOnce:${ref.firestore.app.name}:${ref.path}`, checkStartWithValue(options)); } /** @@ -78,16 +68,9 @@ export function useFirestoreDocOnce( * @param ref - Reference to the document you want to listen to * @param options */ -export function useFirestoreDocData( - ref: firestore.DocumentReference, - options?: ReactFireOptions -): T { +export function useFirestoreDocData(ref: firestore.DocumentReference, options?: ReactFireOptions): T { const idField = checkIdField(options); - return useObservable( - docData(ref, idField), - `firestore:docData:${ref.firestore.app.name}:${ref.path}:idField=${idField}`, - checkStartWithValue(options) - ); + return useObservable(docData(ref, idField), `firestore:docData:${ref.firestore.app.name}:${ref.path}:idField=${idField}`, checkStartWithValue(options)); } /** @@ -96,10 +79,7 @@ export function useFirestoreDocData( * @param ref - Reference to the document you want to get * @param options */ -export function useFirestoreDocDataOnce( - ref: firestore.DocumentReference, - options?: ReactFireOptions -): T { +export function useFirestoreDocDataOnce(ref: firestore.DocumentReference, options?: ReactFireOptions): T { const idField = checkIdField(options); return useObservable( docData(ref, idField).pipe(first()), @@ -118,29 +98,8 @@ export function useFirestoreCollection( query: firestore.Query, options?: ReactFireOptions ): T extends {} ? T[] : firestore.QuerySnapshot { - const queryId = `firestore:collection:${ - query.firestore.app.name - }:${getHashFromFirestoreQuery(query)}`; - - return useObservable( - fromCollectionRef(query), - queryId, - checkStartWithValue(options) - ); -} - -// The Firestore SDK has an undocumented _query -// object that has a method to generate a hash for a query, -// which we need for useObservable -// https://github.com/firebase/firebase-js-sdk/blob/5beb23cd47312ffc415d3ce2ae309cc3a3fde39f/packages/firestore/src/core/query.ts#L221 -interface _QueryWithId extends firestore.Query { - _query: { - canonicalId(): string; - }; -} - -function getHashFromFirestoreQuery(query: firestore.Query) { - return (query as _QueryWithId)._query.canonicalId(); + const queryId = `firestore:collection:${getUniqueIdForFirestoreQuery(query)}`; + return useObservable(fromCollectionRef(query), queryId, checkStartWithValue(options)); } /** @@ -149,18 +108,9 @@ function getHashFromFirestoreQuery(query: firestore.Query) { * @param ref - Reference to the collection you want to listen to * @param options */ -export function useFirestoreCollectionData( - query: firestore.Query, - options?: ReactFireOptions -): T[] { +export function useFirestoreCollectionData(query: firestore.Query, options?: ReactFireOptions): T[] { const idField = checkIdField(options); - const queryId = `firestore:collectionData:${ - query.firestore.app.name - }:${getHashFromFirestoreQuery(query)}:idField=${idField}`; + const queryId = `firestore:collectionData:${getUniqueIdForFirestoreQuery(query)}:idField=${idField}`; - return useObservable( - collectionData(query, idField), - queryId, - checkStartWithValue(options) - ); + return useObservable(collectionData(query, idField), queryId, checkStartWithValue(options)); } diff --git a/reactfire/jest.setup.js b/reactfire/jest.setup.js new file mode 100644 index 00000000..cb7437ec --- /dev/null +++ b/reactfire/jest.setup.js @@ -0,0 +1 @@ +global.globalThis = require('globalthis')(); diff --git a/reactfire/package.json b/reactfire/package.json index cbc538e9..6837ba6c 100644 --- a/reactfire/package.json +++ b/reactfire/package.json @@ -44,9 +44,15 @@ "babel-jest": "^24.9.0", "firebase-functions-test": "^0.1.6", "firebase-tools": "^7.1.0", + "globalthis": "^1.0.1", "jest": "~24.9.0", "react-test-renderer": "^16.9.0", "rollup": "^1.26.3", "typescript": "^3.4.5" + }, + "jest": { + "setupFiles": [ + "../jest.setup.js" + ] } } diff --git a/reactfire/useObservable/index.ts b/reactfire/useObservable/index.ts index 45e3b326..baf9c75c 100644 --- a/reactfire/useObservable/index.ts +++ b/reactfire/useObservable/index.ts @@ -2,18 +2,11 @@ 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; // Since we're side-effect free, we need to ensure our observable cache is global -const preloadedObservables = globalThis[PRELOADED_OBSERVABLES] || new Map>(); +const preloadedObservables: Map> = globalThis[PRELOADED_OBSERVABLES] || new Map(); if (!globalThis[PRELOADED_OBSERVABLES]) { globalThis[PRELOADED_OBSERVABLES] = preloadedObservables; @@ -32,12 +25,7 @@ export function preloadObservable(source: Observable, id: string) { } } -export function useObservable( - source: Observable, - observableId: string, - startWithValue?: T | any, - deps: React.DependencyList = [observableId] -): T { +export function useObservable(source: Observable, observableId: string, startWithValue?: T | any, deps: React.DependencyList = [observableId]): T { if (!observableId) { throw new Error('cannot call useObservable without an observableId'); } @@ -45,9 +33,7 @@ export function useObservable( if (!observable.hasValue && !startWithValue) { throw observable.firstEmission; } - const [latest, setValue] = React.useState(() => - observable.hasValue ? observable.value : startWithValue - ); + const [latest, setValue] = React.useState(() => (observable.hasValue ? observable.value : startWithValue)); React.useEffect(() => { const subscription = observable.subscribe( v => setValue(() => v), diff --git a/yarn.lock b/yarn.lock index 0f014d33..c245514b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6137,6 +6137,13 @@ globals@^12.1.0: dependencies: type-fest "^0.8.1" +globalthis@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.1.tgz#40116f5d9c071f9e8fb0037654df1ab3a83b7ef9" + integrity sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw== + dependencies: + define-properties "^1.1.3" + globby@8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d"