Skip to content

Commit 3462ec5

Browse files
authored
Feature: Add Firestore unwrap data hooks
Feature: Add Firestore unwrap data hooks
2 parents 43a9724 + d986a8d commit 3462ec5

File tree

7 files changed

+13778
-28
lines changed

7 files changed

+13778
-28
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ sample-complex/.cache/**
1313
sample-complex/src/firebase-key.json
1414
reactfire/firestore-debug.log
1515
reactfire/pub/**
16-
pub
16+
pub
17+
yarn-error.log

reactfire/firestore/firestore.test.tsx

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@ import * as firebase from '@firebase/testing';
77
import {
88
useFirestoreDoc,
99
useFirestoreCollection,
10-
FirebaseAppProvider
10+
FirebaseAppProvider,
11+
useFirestoreCollectionData,
12+
useFirestoreDocData,
1113
} from '..';
1214
import { firestore } from 'firebase/app';
1315

1416
describe('Firestore', () => {
15-
let app;
17+
let app: import('firebase').app.App;
1618

1719
beforeAll(async () => {
1820
app = firebase.initializeTestApp({
1921
projectId: '12345',
2022
databaseName: 'my-database',
2123
auth: { uid: 'alice' }
22-
});
24+
}) as import('firebase').app.App;
25+
// TODO(davideast): Wait for rc and analytics to get included in test app
2326
});
2427

2528
afterEach(async () => {
@@ -49,9 +52,7 @@ describe('Firestore', () => {
4952
await ref.set(mockData);
5053

5154
const ReadFirestoreDoc = () => {
52-
const doc = useFirestoreDoc(
53-
(ref as unknown) as firestore.DocumentReference
54-
);
55+
const doc = useFirestoreDoc(ref);
5556

5657
return (
5758
<h1 data-testid="readSuccess">
@@ -72,6 +73,41 @@ describe('Firestore', () => {
7273
expect(getByTestId('readSuccess')).toContainHTML(mockData.a);
7374
});
7475
});
76+
77+
describe('useFirestoreDocData', () => {
78+
it('can get a Firestore document [TEST REQUIRES EMULATOR]', async () => {
79+
const mockData = { a: 'hello' };
80+
81+
const ref = app
82+
.firestore()
83+
.collection('testDoc')
84+
// 'readSuccess' is set to the data-testid={data.id} attribute
85+
.doc('readSuccess');
86+
87+
await ref.set(mockData);
88+
89+
const ReadFirestoreDoc = () => {
90+
const data = useFirestoreDocData<any>(ref, { idField: 'id' });
91+
92+
return (
93+
<h1 data-testid={data.id}>
94+
{data.a}
95+
</h1>
96+
);
97+
};
98+
const { getByTestId } = render(
99+
<FirebaseAppProvider firebase={app}>
100+
<React.Suspense fallback={<h1 data-testid="fallback">Fallback</h1>}>
101+
<ReadFirestoreDoc />
102+
</React.Suspense>
103+
</FirebaseAppProvider>
104+
);
105+
106+
await waitForElement(() => getByTestId('readSuccess'));
107+
108+
expect(getByTestId('readSuccess')).toContainHTML(mockData.a);
109+
});
110+
});
75111

76112
// THIS TEST CAUSES A REACT `act` WARNING
77113
// IT WILL BE FIXED IN REACT 16.9
@@ -87,9 +123,7 @@ describe('Firestore', () => {
87123
await ref.add(mockData2);
88124

89125
const ReadFirestoreCollection = () => {
90-
const collection = useFirestoreCollection(
91-
(ref as unknown) as firestore.CollectionReference
92-
);
126+
const collection = useFirestoreCollection(ref);
93127

94128
return (
95129
<ul data-testid="readSuccess">
@@ -114,4 +148,45 @@ describe('Firestore', () => {
114148
expect(getAllByTestId('listItem').length).toEqual(2);
115149
});
116150
});
151+
152+
// THIS TEST CAUSES A REACT `act` WARNING
153+
// IT WILL BE FIXED IN REACT 16.9
154+
// More info here: https://github.com/testing-library/react-testing-library/issues/281
155+
describe('useFirestoreCollectionData', () => {
156+
it('can get a Firestore collection [TEST REQUIRES EMULATOR]', async () => {
157+
const mockData1 = { a: 'hello' };
158+
const mockData2 = { a: 'goodbye' };
159+
160+
const ref = app.firestore().collection('testCollection');
161+
162+
await ref.add(mockData1);
163+
await ref.add(mockData2);
164+
165+
const ReadFirestoreCollection = () => {
166+
const list = useFirestoreCollectionData<any>(ref, { idField: 'id' });
167+
168+
return (
169+
<ul data-testid="readSuccess">
170+
{list.map(item => (
171+
<li key={item.id} data-testid="listItem">
172+
{item.a}
173+
</li>
174+
))}
175+
</ul>
176+
);
177+
};
178+
const { getAllByTestId } = render(
179+
<FirebaseAppProvider firebase={app}>
180+
<React.Suspense fallback={<h1 data-testid="fallback">Fallback</h1>}>
181+
<ReadFirestoreCollection />
182+
</React.Suspense>
183+
</FirebaseAppProvider>
184+
);
185+
186+
await waitForElement(() => getAllByTestId('listItem'));
187+
188+
expect(getAllByTestId('listItem').length).toEqual(2);
189+
});
190+
});
191+
117192
});

reactfire/firestore/index.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { firestore } from 'firebase/app';
2-
import { doc, fromCollectionRef } from 'rxfire/firestore';
2+
import { doc, collectionData, fromCollectionRef, docData } from 'rxfire/firestore';
33
import { ReactFireOptions, useObservable } from '..';
44

55
/**
@@ -19,6 +19,23 @@ export function useFirestoreDoc<T = unknown>(
1919
);
2020
}
2121

22+
/**
23+
* Suscribe to Firestore Document changes
24+
*
25+
* @param ref - Reference to the document you want to listen to
26+
* @param options
27+
*/
28+
export function useFirestoreDocData<T = unknown>(
29+
ref: firestore.DocumentReference,
30+
options?: ReactFireOptions<T>
31+
): T {
32+
return useObservable(
33+
docData(ref, checkIdField(options)),
34+
ref.path,
35+
checkStartWithValue(options)
36+
);
37+
}
38+
2239
/**
2340
* Subscribe to a Firestore collection
2441
*
@@ -35,3 +52,32 @@ export function useFirestoreCollection<T = { [key: string]: unknown }>(
3552
options ? options.startWithValue : undefined
3653
);
3754
}
55+
56+
/**
57+
* Subscribe to a Firestore collection and unwrap the snapshot.
58+
*
59+
* @param ref - Reference to the collection you want to listen to
60+
* @param options
61+
*/
62+
export function useFirestoreCollectionData<T = { [key: string]: unknown }>(
63+
ref: firestore.CollectionReference,
64+
options?: ReactFireOptions<T[]>
65+
): T[] {
66+
return useObservable(
67+
collectionData(ref, checkIdField(options)),
68+
ref.path,
69+
checkStartWithValue(options)
70+
);
71+
}
72+
73+
function checkOptions(options: ReactFireOptions, field: string) {
74+
return options ? options[field] : undefined;
75+
}
76+
77+
function checkStartWithValue(options: ReactFireOptions) {
78+
return checkOptions(options, 'startWithValue');
79+
}
80+
81+
function checkIdField(options: ReactFireOptions) {
82+
return checkOptions(options, 'idField');
83+
}

reactfire/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface ReactFireOptions<T = unknown> {
2-
startWithValue: T;
2+
startWithValue?: T;
3+
idField?: string;
34
}
45

56
export * from './auth';

reactfire/package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@
2828
"rxjs": "^6.4.0"
2929
},
3030
"devDependencies": {
31-
"firebase": "^7.0.0",
32-
"react": "^16.9.0",
3331
"@babel/core": "^7.4.3",
3432
"@babel/preset-env": "^7.4.3",
3533
"@babel/preset-react": "^7.0.0",
@@ -46,7 +44,7 @@
4644
"jest": "~24.7.1",
4745
"jest-dom": "^3.1.3",
4846
"typescript": "^3.4.5",
49-
"firebase-functions-test": "^0.1.6"
50-
"react-test-renderer": "^16.9.0",
47+
"firebase-functions-test": "^0.1.6",
48+
"react-test-renderer": "^16.9.0"
5149
}
5250
}

sample-simple/src/Firestore.js

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import 'firebase/auth';
22
import 'firebase/firestore';
3-
import '@firebase/performance';
3+
import 'firebase/performance';
44
import React, { useState } from 'react';
55
import {
66
AuthCheck,
77
SuspenseWithPerf,
8-
useFirestoreCollection,
9-
useFirestoreDoc,
8+
useFirestoreCollectionData,
9+
useFirestoreDocData,
1010
useFirebaseApp
1111
} from 'reactfire';
1212

@@ -20,13 +20,12 @@ const Counter = props => {
2020
});
2121
};
2222

23-
const snapshot = useFirestoreDoc(ref);
24-
const counterValue = snapshot.data().value;
23+
const { value } = useFirestoreDocData(ref);
2524

2625
return (
2726
<>
2827
<button onClick={() => increment(-1)}>-</button>
29-
<span> {counterValue} </span>
28+
<span> {value} </span>
3029
<button onClick={() => increment(1)}>+</button>
3130
</>
3231
);
@@ -62,7 +61,7 @@ const AnimalEntry = ({ saveAnimal }) => {
6261
const List = props => {
6362
const firebaseApp = useFirebaseApp();
6463
const ref = firebaseApp.firestore().collection('animals');
65-
const snapShot = useFirestoreCollection(ref);
64+
const animals = useFirestoreCollectionData(ref, { idField: 'id' });
6665

6766
const addNewAnimal = commonName =>
6867
ref.add({
@@ -75,11 +74,11 @@ const List = props => {
7574
<>
7675
<AnimalEntry saveAnimal={addNewAnimal} />
7776
<ul>
78-
{snapShot.docs.map(snap => (
79-
<li key={snap.id}>
80-
{snap.get('commonName')}{' '}
81-
<button onClick={() => removeAnimal(snap.id)}>X</button>
82-
</li>
77+
{animals.map(animal => (
78+
<li key={animal.id}>
79+
{animal.commonName}{' '}
80+
<button onClick={() => removeAnimal(animal.id)}>X</button>
81+
</li>
8382
))}
8483
</ul>
8584
</>

0 commit comments

Comments
 (0)