Skip to content

Commit ca32a93

Browse files
Add hooks to get firestore document data once (#211)
* Add hooks to get firestore document data once * use checkStartWithValue for consistency Co-authored-by: James Daniels <[email protected]>
1 parent 121f5e1 commit ca32a93

File tree

4 files changed

+164
-2
lines changed

4 files changed

+164
-2
lines changed

docs/reference.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
- Database
2323
- Cloud Firestore
2424
- [`useFirestoreDoc`](#useFirestoreDoc)
25+
- [`useFirestoreDocOnce`](#useFirestoreDocOnce)
2526
- [`useFirestoreDocData`](#useFirestoreDocData)
27+
- [`useFirestoreDocDataOnce`](#useFirestoreDocDataOnce)
2628
- [`useFirestoreCollection`](#useFirestoreCollection)
2729
- [`useFirestoreCollectionData`](#useFirestoreCollectionData)
2830
- Realtime Database
@@ -191,6 +193,23 @@ _Throws a Promise by default_
191193

192194
[`DocumentSnapshot`](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentSnapshot)
193195

196+
### `useFirestoreDocOnce`
197+
198+
Get a firestore document, but don't listen for updates.
199+
200+
_Throws a Promise by default_
201+
202+
#### Parameters
203+
204+
| Parameter | Type | Description |
205+
| ----------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
206+
| ref | [`DocumentReference`](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentReference) | A reference to the document you want to listen to |
207+
| options _?_ | ReactFireOptions | Options. This hook will not throw a Promise if you provide `startWithValue`. |
208+
209+
#### Returns
210+
211+
[`DocumentSnapshot`](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentSnapshot)
212+
194213
### `useFirestoreDocData`
195214

196215
Listen to a Firestore Document.
@@ -208,6 +227,23 @@ _Throws a Promise by default_
208227

209228
`T`
210229

230+
### `useFirestoreDocDataOnce`
231+
232+
Get a firestore document, but don't listen for updates.
233+
234+
_Throws a Promise by default_
235+
236+
#### Parameters
237+
238+
| Parameter | Type | Description |
239+
| ----------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
240+
| ref | [`DocumentReference`](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentReference) | A reference to the document you want to listen to |
241+
| options _?_ | ReactFireOptions | Options. This hook will not throw a Promise if you provide `startWithValue`. |
242+
243+
#### Returns
244+
245+
`T`
246+
211247
### `useFirestoreCollection`
212248

213249
Listen to a Firestore Collection.

reactfire/firestore/firestore.test.tsx

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import {
88
useFirestoreCollection,
99
FirebaseAppProvider,
1010
useFirestoreCollectionData,
11-
useFirestoreDocData
11+
useFirestoreDocData,
12+
useFirestoreDocDataOnce,
13+
useFirestoreDocOnce
1214
} from '..';
1315
import { firestore } from 'firebase/app';
1416

@@ -104,6 +106,75 @@ describe('Firestore', () => {
104106
});
105107
});
106108

109+
describe('useFirestoreDocOnce', () => {
110+
it('works when the document does not exist, and does not update when it is created', async () => {
111+
const ref = app
112+
.firestore()
113+
.collection('testDoc')
114+
.doc('emptydoc');
115+
116+
const ReadFirestoreDoc = () => {
117+
const dataOnce = useFirestoreDocOnce<any>(ref);
118+
119+
return (
120+
<>
121+
<h1 data-testid="once">{dataOnce.exists.toString()}</h1>
122+
</>
123+
);
124+
};
125+
const { getByTestId } = render(
126+
<FirebaseAppProvider firebase={app}>
127+
<React.Suspense fallback={<h1 data-testid="fallback">Fallback</h1>}>
128+
<ReadFirestoreDoc />
129+
</React.Suspense>
130+
</FirebaseAppProvider>
131+
);
132+
133+
await waitForElement(() => getByTestId('once'));
134+
expect(getByTestId('once')).toContainHTML('false');
135+
136+
await act(() => ref.set({ a: 'test' }));
137+
expect(getByTestId('once')).toContainHTML('false');
138+
});
139+
});
140+
141+
describe('useFirestoreDocDataOnce', () => {
142+
it('does not update on database changes [TEST REQUIRES EMULATOR]', async () => {
143+
const mockData1 = { a: 'hello' };
144+
const mockData2 = { a: 'goodbye' };
145+
146+
const ref = app
147+
.firestore()
148+
.collection('testDoc')
149+
.doc('readSuccess');
150+
151+
await ref.set(mockData1);
152+
153+
const ReadFirestoreDoc = () => {
154+
const dataOnce = useFirestoreDocDataOnce<any>(ref, { idField: 'id' });
155+
156+
return (
157+
<>
158+
<h1 data-testid="once">{dataOnce.a}</h1>{' '}
159+
</>
160+
);
161+
};
162+
const { getByTestId } = render(
163+
<FirebaseAppProvider firebase={app}>
164+
<React.Suspense fallback={<h1 data-testid="fallback">Fallback</h1>}>
165+
<ReadFirestoreDoc />
166+
</React.Suspense>
167+
</FirebaseAppProvider>
168+
);
169+
170+
await waitForElement(() => getByTestId('once'));
171+
expect(getByTestId('once')).toContainHTML(mockData1.a);
172+
173+
await act(() => ref.set(mockData2));
174+
expect(getByTestId('once')).toContainHTML(mockData1.a);
175+
});
176+
});
177+
107178
describe('useFirestoreCollection', () => {
108179
it('can get a Firestore collection [TEST REQUIRES EMULATOR]', async () => {
109180
const mockData1 = { a: 'hello' };

reactfire/firestore/index.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
checkStartWithValue
1414
} from '..';
1515
import { preloadObservable } from '../useObservable';
16+
import { first } from 'rxjs/operators';
1617

1718
// starts a request for a firestore doc.
1819
// imports the firestore SDK automatically
@@ -52,6 +53,23 @@ export function useFirestoreDoc<T = unknown>(
5253
);
5354
}
5455

56+
/**
57+
* Get a firestore document and don't subscribe to changes
58+
*
59+
* @param ref - Reference to the document you want to get
60+
* @param options
61+
*/
62+
export function useFirestoreDocOnce<T = unknown>(
63+
ref: firestore.DocumentReference,
64+
options?: ReactFireOptions<T>
65+
): T extends {} ? T : firestore.DocumentSnapshot {
66+
return useObservable(
67+
doc(ref).pipe(first()),
68+
`firestore:docOnce:${ref.firestore.app.name}:${ref.path}`,
69+
checkStartWithValue(options)
70+
);
71+
}
72+
5573
/**
5674
* Suscribe to Firestore Document changes
5775
*
@@ -70,6 +88,24 @@ export function useFirestoreDocData<T = unknown>(
7088
);
7189
}
7290

91+
/**
92+
* Get a firestore document and don't subscribe to changes
93+
*
94+
* @param ref - Reference to the document you want to get
95+
* @param options
96+
*/
97+
export function useFirestoreDocDataOnce<T = unknown>(
98+
ref: firestore.DocumentReference,
99+
options?: ReactFireOptions<T>
100+
): T {
101+
const idField = checkIdField(options);
102+
return useObservable(
103+
docData(ref, idField).pipe(first()),
104+
`firestore:docDataOnce:${ref.firestore.app.name}:${ref.path}:idField=${idField}`,
105+
checkStartWithValue(options)
106+
);
107+
}
108+
73109
/**
74110
* Subscribe to a Firestore collection
75111
*
@@ -87,7 +123,7 @@ export function useFirestoreCollection<T = { [key: string]: unknown }>(
87123
return useObservable(
88124
fromCollectionRef(query),
89125
queryId,
90-
options ? options.startWithValue : undefined
126+
checkStartWithValue(options)
91127
);
92128
}
93129

sample/src/Firestore.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
SuspenseWithPerf,
66
useFirestoreCollectionData,
77
useFirestoreDocData,
8+
useFirestoreDocDataOnce,
89
useFirestore
910
} from 'reactfire';
1011

@@ -32,6 +33,16 @@ const Counter = props => {
3233
);
3334
};
3435

36+
const StaticValue = props => {
37+
const firestore = useFirestore();
38+
39+
const ref = firestore().doc('count/counter');
40+
41+
const { value } = useFirestoreDocDataOnce(ref);
42+
43+
return <span>{value}</span>;
44+
};
45+
3546
const AnimalEntry = ({ saveAnimal }) => {
3647
const [text, setText] = useState('');
3748
const [disabled, setDisabled] = useState(false);
@@ -121,6 +132,14 @@ const SuspenseWrapper = props => {
121132
>
122133
<Counter />
123134
</SuspenseWithPerf>
135+
<h3>Sample One-time Get</h3>
136+
<SuspenseWithPerf
137+
fallback="connecting to Firestore..."
138+
traceId="firestore-demo-doc"
139+
>
140+
<StaticValue />
141+
</SuspenseWithPerf>
142+
124143
<h3>Sample Collection Listener</h3>
125144
<SuspenseWithPerf
126145
fallback="connecting to Firestore..."

0 commit comments

Comments
 (0)