Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 */
Expand All @@ -70,10 +67,7 @@ function App() {
return (
<FirebaseAppProvider firebaseConfig={firebaseConfig}>
<h1>🌯</h1>
<SuspenseWithPerf
fallback={<p>loading burrito status...</p>}
traceId={'load-burrito-status'}
>
<SuspenseWithPerf fallback={<p>loading burrito status...</p>} traceId={'load-burrito-status'}>
<Burrito />
</SuspenseWithPerf>
</FirebaseAppProvider>
Expand Down
72 changes: 26 additions & 46 deletions reactfire/database/index.tsx
Original file line number Diff line number Diff line change
@@ -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<database.Query> = 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<T = unknown>(
ref: database.Reference,
options?: ReactFireOptions<T>
): QueryChange | T {
return useObservable(
object(ref),
`database:object:${ref.toString()}`,
options ? options.startWithValue : undefined
);
export function useDatabaseObject<T = unknown>(ref: database.Reference, options?: ReactFireOptions<T>): QueryChange | T {
return useObservable(object(ref), `database:object:${ref.toString()}`, options ? options.startWithValue : undefined);
}

// ============================================================================
Expand All @@ -50,23 +55,9 @@ function changeToData(change: QueryChange, keyField?: string): {} {
}
// ============================================================================

export function useDatabaseObjectData<T>(
ref: database.Reference,
options?: ReactFireOptions<T>
): T {
export function useDatabaseObjectData<T>(ref: database.Reference, options?: ReactFireOptions<T>): 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));
}

/**
Expand All @@ -79,23 +70,12 @@ export function useDatabaseList<T = { [key: string]: unknown }>(
ref: database.Reference | database.Query,
options?: ReactFireOptions<T[]>
): 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<T = { [key: string]: unknown }>(
ref: database.Reference | database.Query,
options?: ReactFireOptions<T[]>
): T[] {
export function useDatabaseListData<T = { [key: string]: unknown }>(ref: database.Reference | database.Query, options?: ReactFireOptions<T[]>): 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));
}
116 changes: 33 additions & 83 deletions reactfire/firestore/index.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,41 @@
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<firestore.Query> = 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.
//
// 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}`);
});
}

Expand All @@ -44,15 +45,8 @@ export function preloadFirestoreDoc(
* @param ref - Reference to the document you want to listen to
* @param options
*/
export function useFirestoreDoc<T = unknown>(
ref: firestore.DocumentReference,
options?: ReactFireOptions<T>
): T extends {} ? T : firestore.DocumentSnapshot {
return useObservable(
doc(ref),
`firestore:doc:${ref.firestore.app.name}:${ref.path}`,
options ? options.startWithValue : undefined
);
export function useFirestoreDoc<T = unknown>(ref: firestore.DocumentReference, options?: ReactFireOptions<T>): T extends {} ? T : firestore.DocumentSnapshot {
return useObservable(doc(ref), `firestore:doc:${ref.firestore.app.name}:${ref.path}`, options ? options.startWithValue : undefined);
}

/**
Expand All @@ -65,11 +59,7 @@ export function useFirestoreDocOnce<T = unknown>(
ref: firestore.DocumentReference,
options?: ReactFireOptions<T>
): 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));
}

/**
Expand All @@ -78,16 +68,9 @@ export function useFirestoreDocOnce<T = unknown>(
* @param ref - Reference to the document you want to listen to
* @param options
*/
export function useFirestoreDocData<T = unknown>(
ref: firestore.DocumentReference,
options?: ReactFireOptions<T>
): T {
export function useFirestoreDocData<T = unknown>(ref: firestore.DocumentReference, options?: ReactFireOptions<T>): 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));
}

/**
Expand All @@ -96,10 +79,7 @@ export function useFirestoreDocData<T = unknown>(
* @param ref - Reference to the document you want to get
* @param options
*/
export function useFirestoreDocDataOnce<T = unknown>(
ref: firestore.DocumentReference,
options?: ReactFireOptions<T>
): T {
export function useFirestoreDocDataOnce<T = unknown>(ref: firestore.DocumentReference, options?: ReactFireOptions<T>): T {
const idField = checkIdField(options);
return useObservable(
docData(ref, idField).pipe(first()),
Expand All @@ -118,29 +98,8 @@ export function useFirestoreCollection<T = { [key: string]: unknown }>(
query: firestore.Query,
options?: ReactFireOptions<T[]>
): 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));
}

/**
Expand All @@ -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<T = { [key: string]: unknown }>(
query: firestore.Query,
options?: ReactFireOptions<T[]>
): T[] {
export function useFirestoreCollectionData<T = { [key: string]: unknown }>(query: firestore.Query, options?: ReactFireOptions<T[]>): 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));
}
1 change: 1 addition & 0 deletions reactfire/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global.globalThis = require('globalthis')();
6 changes: 6 additions & 0 deletions reactfire/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
}
20 changes: 3 additions & 17 deletions reactfire/useObservable/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, SuspenseSubject<unknown>>();
const preloadedObservables: Map<string, SuspenseSubject<unknown>> = globalThis[PRELOADED_OBSERVABLES] || new Map();

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

export function useObservable<T>(
source: Observable<T | any>,
observableId: string,
startWithValue?: T | any,
deps: React.DependencyList = [observableId]
): T {
export function useObservable<T>(source: Observable<T | any>, observableId: string, startWithValue?: T | any, deps: React.DependencyList = [observableId]): T {
if (!observableId) {
throw new Error('cannot call useObservable without an observableId');
}
const observable = preloadObservable(source, observableId);
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),
Expand Down
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down