Skip to content

Commit 0de1068

Browse files
authored
Don't use JS SDK private APIs. (#224)
* Don't use JS SDK private APIs. * Add note about polyfills
1 parent 5487096 commit 0de1068

File tree

7 files changed

+80
-156
lines changed

7 files changed

+80
-156
lines changed

README.md

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ npm install --save reactfire firebase
2828
yarn add reactfire firebase
2929
```
3030

31+
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).
32+
3133
- [**Quickstart**](./docs/quickstart.md)
3234
- [**Common Use Cases**](./docs/use.md)
3335
- [**API Reference**](./docs/reference.md)
@@ -40,12 +42,7 @@ Check out the
4042
```jsx
4143
import React, { Component } from 'react';
4244
import { createRoot } from 'react-dom';
43-
import {
44-
FirebaseAppProvider,
45-
useFirestoreDocData,
46-
useFirestore,
47-
SuspenseWithPerf
48-
} from 'reactfire';
45+
import { FirebaseAppProvider, useFirestoreDocData, useFirestore, SuspenseWithPerf } from 'reactfire';
4946

5047
const firebaseConfig = {
5148
/* Add your config from the Firebase Console */
@@ -70,10 +67,7 @@ function App() {
7067
return (
7168
<FirebaseAppProvider firebaseConfig={firebaseConfig}>
7269
<h1>🌯</h1>
73-
<SuspenseWithPerf
74-
fallback={<p>loading burrito status...</p>}
75-
traceId={'load-burrito-status'}
76-
>
70+
<SuspenseWithPerf fallback={<p>loading burrito status...</p>} traceId={'load-burrito-status'}>
7771
<Burrito />
7872
</SuspenseWithPerf>
7973
</FirebaseAppProvider>

reactfire/database/index.tsx

Lines changed: 26 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,35 @@
11
import { database } from 'firebase/app';
22
import { list, object, QueryChange, listVal } from 'rxfire/database';
3-
import {
4-
ReactFireOptions,
5-
useObservable,
6-
checkIdField,
7-
checkStartWithValue
8-
} from '..';
3+
import { ReactFireOptions, useObservable, checkIdField, checkStartWithValue } from '..';
94

105
import { Observable } from 'rxjs';
116
import { map } from 'rxjs/operators';
127

8+
const CACHED_QUERIES = '_reactFireDatabaseCachedQueries';
9+
10+
// Since we're side-effect free, we need to ensure our observableId cache is global
11+
const cachedQueries: Array<database.Query> = globalThis[CACHED_QUERIES] || [];
12+
13+
if (!globalThis[CACHED_QUERIES]) {
14+
globalThis[CACHED_QUERIES] = cachedQueries;
15+
}
16+
17+
function getUniqueIdForDatabaseQuery(query: database.Query) {
18+
const index = cachedQueries.findIndex(cachedQuery => cachedQuery.isEqual(query));
19+
if (index > -1) {
20+
return index;
21+
}
22+
return cachedQueries.push(query) - 1;
23+
}
24+
1325
/**
1426
* Subscribe to a Realtime Database object
1527
*
1628
* @param ref - Reference to the DB object you want to listen to
1729
* @param options
1830
*/
19-
export function useDatabaseObject<T = unknown>(
20-
ref: database.Reference,
21-
options?: ReactFireOptions<T>
22-
): QueryChange | T {
23-
return useObservable(
24-
object(ref),
25-
`database:object:${ref.toString()}`,
26-
options ? options.startWithValue : undefined
27-
);
31+
export function useDatabaseObject<T = unknown>(ref: database.Reference, options?: ReactFireOptions<T>): QueryChange | T {
32+
return useObservable(object(ref), `database:object:${ref.toString()}`, options ? options.startWithValue : undefined);
2833
}
2934

3035
// ============================================================================
@@ -50,23 +55,9 @@ function changeToData(change: QueryChange, keyField?: string): {} {
5055
}
5156
// ============================================================================
5257

53-
export function useDatabaseObjectData<T>(
54-
ref: database.Reference,
55-
options?: ReactFireOptions<T>
56-
): T {
58+
export function useDatabaseObjectData<T>(ref: database.Reference, options?: ReactFireOptions<T>): T {
5759
const idField = checkIdField(options);
58-
return useObservable(
59-
objectVal(ref, idField),
60-
`database:objectVal:${ref.toString()}:idField=${idField}`,
61-
checkStartWithValue(options)
62-
);
63-
}
64-
65-
// Realtime Database has an undocumented method
66-
// that helps us build a unique ID for the query
67-
// https://github.com/firebase/firebase-js-sdk/blob/aca99669dd8ed096f189578c47a56a8644ac62e6/packages/database/src/api/Query.ts#L601
68-
interface _QueryWithId extends database.Query {
69-
queryIdentifier(): string;
60+
return useObservable(objectVal(ref, idField), `database:objectVal:${ref.toString()}:idField=${idField}`, checkStartWithValue(options));
7061
}
7162

7263
/**
@@ -79,23 +70,12 @@ export function useDatabaseList<T = { [key: string]: unknown }>(
7970
ref: database.Reference | database.Query,
8071
options?: ReactFireOptions<T[]>
8172
): QueryChange[] | T[] {
82-
const hash = `database:list:${ref.toString()}|${(ref as _QueryWithId).queryIdentifier()}`;
73+
const hash = `database:list:${getUniqueIdForDatabaseQuery(ref)}`;
8374

84-
return useObservable(
85-
list(ref),
86-
hash,
87-
options ? options.startWithValue : undefined
88-
);
75+
return useObservable(list(ref), hash, options ? options.startWithValue : undefined);
8976
}
9077

91-
export function useDatabaseListData<T = { [key: string]: unknown }>(
92-
ref: database.Reference | database.Query,
93-
options?: ReactFireOptions<T[]>
94-
): T[] {
78+
export function useDatabaseListData<T = { [key: string]: unknown }>(ref: database.Reference | database.Query, options?: ReactFireOptions<T[]>): T[] {
9579
const idField = checkIdField(options);
96-
return useObservable(
97-
listVal(ref, idField),
98-
`database:listVal:${ref.toString()}|${(ref as _QueryWithId).queryIdentifier()}:idField=${idField}`,
99-
checkStartWithValue(options)
100-
);
80+
return useObservable(listVal(ref, idField), `database:listVal:${getUniqueIdForDatabaseQuery(ref)}:idField=${idField}`, checkStartWithValue(options));
10181
}

reactfire/firestore/index.tsx

Lines changed: 33 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,41 @@
11
import { firestore } from 'firebase/app';
2-
import {
3-
collectionData,
4-
doc,
5-
docData,
6-
fromCollectionRef
7-
} from 'rxfire/firestore';
8-
import {
9-
preloadFirestore,
10-
ReactFireOptions,
11-
useObservable,
12-
checkIdField,
13-
checkStartWithValue
14-
} from '..';
2+
import { collectionData, doc, docData, fromCollectionRef } from 'rxfire/firestore';
3+
import { preloadFirestore, ReactFireOptions, useObservable, checkIdField, checkStartWithValue } from '..';
154
import { preloadObservable } from '../useObservable';
165
import { first } from 'rxjs/operators';
176
import { useFirebaseApp } from '../firebaseApp';
187

8+
const CACHED_QUERIES = '_reactFireFirestoreQueryCache';
9+
10+
// Since we're side-effect free, we need to ensure our observableId cache is global
11+
const cachedQueries: Array<firestore.Query> = globalThis[CACHED_QUERIES] || [];
12+
13+
if (!globalThis[CACHED_QUERIES]) {
14+
globalThis[CACHED_QUERIES] = cachedQueries;
15+
}
16+
17+
function getUniqueIdForFirestoreQuery(query: firestore.Query) {
18+
const index = cachedQueries.findIndex(cachedQuery => cachedQuery.isEqual(query));
19+
if (index > -1) {
20+
return index;
21+
}
22+
return cachedQueries.push(query) - 1;
23+
}
24+
1925
// starts a request for a firestore doc.
2026
// imports the firestore SDK automatically
2127
// if it hasn't been imported yet.
2228
//
2329
// there's a decent chance this gets called before the Firestore SDK
2430
// has been imported, so it takes a refProvider instead of a ref
2531
export function preloadFirestoreDoc(
26-
refProvider: (
27-
firestore: firebase.firestore.Firestore
28-
) => firestore.DocumentReference,
32+
refProvider: (firestore: firebase.firestore.Firestore) => firestore.DocumentReference,
2933
options?: { firebaseApp?: firebase.app.App }
3034
) {
3135
const firebaseApp = options?.firebaseApp || useFirebaseApp();
32-
return preloadFirestore({firebaseApp}).then(firestore => {
36+
return preloadFirestore({ firebaseApp }).then(firestore => {
3337
const ref = refProvider(firestore());
34-
return preloadObservable(
35-
doc(ref),
36-
`firestore:doc:${firebaseApp.name}:${ref.path}`
37-
);
38+
return preloadObservable(doc(ref), `firestore:doc:${firebaseApp.name}:${ref.path}`);
3839
});
3940
}
4041

@@ -44,15 +45,8 @@ export function preloadFirestoreDoc(
4445
* @param ref - Reference to the document you want to listen to
4546
* @param options
4647
*/
47-
export function useFirestoreDoc<T = unknown>(
48-
ref: firestore.DocumentReference,
49-
options?: ReactFireOptions<T>
50-
): T extends {} ? T : firestore.DocumentSnapshot {
51-
return useObservable(
52-
doc(ref),
53-
`firestore:doc:${ref.firestore.app.name}:${ref.path}`,
54-
options ? options.startWithValue : undefined
55-
);
48+
export function useFirestoreDoc<T = unknown>(ref: firestore.DocumentReference, options?: ReactFireOptions<T>): T extends {} ? T : firestore.DocumentSnapshot {
49+
return useObservable(doc(ref), `firestore:doc:${ref.firestore.app.name}:${ref.path}`, options ? options.startWithValue : undefined);
5650
}
5751

5852
/**
@@ -65,11 +59,7 @@ export function useFirestoreDocOnce<T = unknown>(
6559
ref: firestore.DocumentReference,
6660
options?: ReactFireOptions<T>
6761
): T extends {} ? T : firestore.DocumentSnapshot {
68-
return useObservable(
69-
doc(ref).pipe(first()),
70-
`firestore:docOnce:${ref.firestore.app.name}:${ref.path}`,
71-
checkStartWithValue(options)
72-
);
62+
return useObservable(doc(ref).pipe(first()), `firestore:docOnce:${ref.firestore.app.name}:${ref.path}`, checkStartWithValue(options));
7363
}
7464

7565
/**
@@ -78,16 +68,9 @@ export function useFirestoreDocOnce<T = unknown>(
7868
* @param ref - Reference to the document you want to listen to
7969
* @param options
8070
*/
81-
export function useFirestoreDocData<T = unknown>(
82-
ref: firestore.DocumentReference,
83-
options?: ReactFireOptions<T>
84-
): T {
71+
export function useFirestoreDocData<T = unknown>(ref: firestore.DocumentReference, options?: ReactFireOptions<T>): T {
8572
const idField = checkIdField(options);
86-
return useObservable(
87-
docData(ref, idField),
88-
`firestore:docData:${ref.firestore.app.name}:${ref.path}:idField=${idField}`,
89-
checkStartWithValue(options)
90-
);
73+
return useObservable(docData(ref, idField), `firestore:docData:${ref.firestore.app.name}:${ref.path}:idField=${idField}`, checkStartWithValue(options));
9174
}
9275

9376
/**
@@ -96,10 +79,7 @@ export function useFirestoreDocData<T = unknown>(
9679
* @param ref - Reference to the document you want to get
9780
* @param options
9881
*/
99-
export function useFirestoreDocDataOnce<T = unknown>(
100-
ref: firestore.DocumentReference,
101-
options?: ReactFireOptions<T>
102-
): T {
82+
export function useFirestoreDocDataOnce<T = unknown>(ref: firestore.DocumentReference, options?: ReactFireOptions<T>): T {
10383
const idField = checkIdField(options);
10484
return useObservable(
10585
docData(ref, idField).pipe(first()),
@@ -118,29 +98,8 @@ export function useFirestoreCollection<T = { [key: string]: unknown }>(
11898
query: firestore.Query,
11999
options?: ReactFireOptions<T[]>
120100
): T extends {} ? T[] : firestore.QuerySnapshot {
121-
const queryId = `firestore:collection:${
122-
query.firestore.app.name
123-
}:${getHashFromFirestoreQuery(query)}`;
124-
125-
return useObservable(
126-
fromCollectionRef(query),
127-
queryId,
128-
checkStartWithValue(options)
129-
);
130-
}
131-
132-
// The Firestore SDK has an undocumented _query
133-
// object that has a method to generate a hash for a query,
134-
// which we need for useObservable
135-
// https://github.com/firebase/firebase-js-sdk/blob/5beb23cd47312ffc415d3ce2ae309cc3a3fde39f/packages/firestore/src/core/query.ts#L221
136-
interface _QueryWithId extends firestore.Query {
137-
_query: {
138-
canonicalId(): string;
139-
};
140-
}
141-
142-
function getHashFromFirestoreQuery(query: firestore.Query) {
143-
return (query as _QueryWithId)._query.canonicalId();
101+
const queryId = `firestore:collection:${getUniqueIdForFirestoreQuery(query)}`;
102+
return useObservable(fromCollectionRef(query), queryId, checkStartWithValue(options));
144103
}
145104

146105
/**
@@ -149,18 +108,9 @@ function getHashFromFirestoreQuery(query: firestore.Query) {
149108
* @param ref - Reference to the collection you want to listen to
150109
* @param options
151110
*/
152-
export function useFirestoreCollectionData<T = { [key: string]: unknown }>(
153-
query: firestore.Query,
154-
options?: ReactFireOptions<T[]>
155-
): T[] {
111+
export function useFirestoreCollectionData<T = { [key: string]: unknown }>(query: firestore.Query, options?: ReactFireOptions<T[]>): T[] {
156112
const idField = checkIdField(options);
157-
const queryId = `firestore:collectionData:${
158-
query.firestore.app.name
159-
}:${getHashFromFirestoreQuery(query)}:idField=${idField}`;
113+
const queryId = `firestore:collectionData:${getUniqueIdForFirestoreQuery(query)}:idField=${idField}`;
160114

161-
return useObservable(
162-
collectionData(query, idField),
163-
queryId,
164-
checkStartWithValue(options)
165-
);
115+
return useObservable(collectionData(query, idField), queryId, checkStartWithValue(options));
166116
}

reactfire/jest.setup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
global.globalThis = require('globalthis')();

reactfire/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,15 @@
4444
"babel-jest": "^24.9.0",
4545
"firebase-functions-test": "^0.1.6",
4646
"firebase-tools": "^7.1.0",
47+
"globalthis": "^1.0.1",
4748
"jest": "~24.9.0",
4849
"react-test-renderer": "^16.9.0",
4950
"rollup": "^1.26.3",
5051
"typescript": "^3.4.5"
52+
},
53+
"jest": {
54+
"setupFiles": [
55+
"../jest.setup.js"
56+
]
5157
}
5258
}

reactfire/useObservable/index.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,11 @@ import * as React from 'react';
22
import { Observable } from 'rxjs';
33
import { SuspenseSubject } from './SuspenseSubject';
44

5-
const globalThis = function() {
6-
if (typeof self !== 'undefined') { return self; }
7-
if (typeof window !== 'undefined') { return window; }
8-
if (typeof global !== 'undefined') { return global; }
9-
throw new Error('unable to locate global object');
10-
}();
11-
125
const PRELOADED_OBSERVABLES = '_reactFirePreloadedObservables';
136
const DEFAULT_TIMEOUT = 30_000;
147

158
// Since we're side-effect free, we need to ensure our observable cache is global
16-
const preloadedObservables = globalThis[PRELOADED_OBSERVABLES] || new Map<string, SuspenseSubject<unknown>>();
9+
const preloadedObservables: Map<string, SuspenseSubject<unknown>> = globalThis[PRELOADED_OBSERVABLES] || new Map();
1710

1811
if (!globalThis[PRELOADED_OBSERVABLES]) {
1912
globalThis[PRELOADED_OBSERVABLES] = preloadedObservables;
@@ -32,22 +25,15 @@ export function preloadObservable<T>(source: Observable<T>, id: string) {
3225
}
3326
}
3427

35-
export function useObservable<T>(
36-
source: Observable<T | any>,
37-
observableId: string,
38-
startWithValue?: T | any,
39-
deps: React.DependencyList = [observableId]
40-
): T {
28+
export function useObservable<T>(source: Observable<T | any>, observableId: string, startWithValue?: T | any, deps: React.DependencyList = [observableId]): T {
4129
if (!observableId) {
4230
throw new Error('cannot call useObservable without an observableId');
4331
}
4432
const observable = preloadObservable(source, observableId);
4533
if (!observable.hasValue && !startWithValue) {
4634
throw observable.firstEmission;
4735
}
48-
const [latest, setValue] = React.useState(() =>
49-
observable.hasValue ? observable.value : startWithValue
50-
);
36+
const [latest, setValue] = React.useState(() => (observable.hasValue ? observable.value : startWithValue));
5137
React.useEffect(() => {
5238
const subscription = observable.subscribe(
5339
v => setValue(() => v),

yarn.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6137,6 +6137,13 @@ globals@^12.1.0:
61376137
dependencies:
61386138
type-fest "^0.8.1"
61396139

6140+
globalthis@^1.0.1:
6141+
version "1.0.1"
6142+
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.1.tgz#40116f5d9c071f9e8fb0037654df1ab3a83b7ef9"
6143+
integrity sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw==
6144+
dependencies:
6145+
define-properties "^1.1.3"
6146+
61406147
61416148
version "8.0.2"
61426149
resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d"

0 commit comments

Comments
 (0)