diff --git a/docs/reference.md b/docs/reference.md index 50eda72e..495375e3 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -22,7 +22,9 @@ - Database - Cloud Firestore - [`useFirestoreDoc`](#useFirestoreDoc) + - [`useFirestoreDocOnce`](#useFirestoreDocOnce) - [`useFirestoreDocData`](#useFirestoreDocData) + - [`useFirestoreDocDataOnce`](#useFirestoreDocDataOnce) - [`useFirestoreCollection`](#useFirestoreCollection) - [`useFirestoreCollectionData`](#useFirestoreCollectionData) - Realtime Database @@ -191,6 +193,23 @@ _Throws a Promise by default_ [`DocumentSnapshot`](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentSnapshot) +### `useFirestoreDocOnce` + +Get a firestore document, but don't listen for updates. + +_Throws a Promise by default_ + +#### Parameters + +| Parameter | Type | Description | +| ----------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| ref | [`DocumentReference`](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentReference) | A reference to the document you want to listen to | +| options _?_ | ReactFireOptions | Options. This hook will not throw a Promise if you provide `startWithValue`. | + +#### Returns + +[`DocumentSnapshot`](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentSnapshot) + ### `useFirestoreDocData` Listen to a Firestore Document. @@ -208,6 +227,23 @@ _Throws a Promise by default_ `T` +### `useFirestoreDocDataOnce` + +Get a firestore document, but don't listen for updates. + +_Throws a Promise by default_ + +#### Parameters + +| Parameter | Type | Description | +| ----------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| ref | [`DocumentReference`](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentReference) | A reference to the document you want to listen to | +| options _?_ | ReactFireOptions | Options. This hook will not throw a Promise if you provide `startWithValue`. | + +#### Returns + +`T` + ### `useFirestoreCollection` Listen to a Firestore Collection. diff --git a/reactfire/firestore/firestore.test.tsx b/reactfire/firestore/firestore.test.tsx index d1637697..b32056d6 100644 --- a/reactfire/firestore/firestore.test.tsx +++ b/reactfire/firestore/firestore.test.tsx @@ -8,7 +8,9 @@ import { useFirestoreCollection, FirebaseAppProvider, useFirestoreCollectionData, - useFirestoreDocData + useFirestoreDocData, + useFirestoreDocDataOnce, + useFirestoreDocOnce } from '..'; import { firestore } from 'firebase/app'; @@ -104,6 +106,75 @@ describe('Firestore', () => { }); }); + describe('useFirestoreDocOnce', () => { + it('works when the document does not exist, and does not update when it is created', async () => { + const ref = app + .firestore() + .collection('testDoc') + .doc('emptydoc'); + + const ReadFirestoreDoc = () => { + const dataOnce = useFirestoreDocOnce(ref); + + return ( + <> +

{dataOnce.exists.toString()}

+ + ); + }; + const { getByTestId } = render( + + Fallback}> + + + + ); + + await waitForElement(() => getByTestId('once')); + expect(getByTestId('once')).toContainHTML('false'); + + await act(() => ref.set({ a: 'test' })); + expect(getByTestId('once')).toContainHTML('false'); + }); + }); + + describe('useFirestoreDocDataOnce', () => { + it('does not update on database changes [TEST REQUIRES EMULATOR]', async () => { + const mockData1 = { a: 'hello' }; + const mockData2 = { a: 'goodbye' }; + + const ref = app + .firestore() + .collection('testDoc') + .doc('readSuccess'); + + await ref.set(mockData1); + + const ReadFirestoreDoc = () => { + const dataOnce = useFirestoreDocDataOnce(ref, { idField: 'id' }); + + return ( + <> +

{dataOnce.a}

{' '} + + ); + }; + const { getByTestId } = render( + + Fallback}> + + + + ); + + await waitForElement(() => getByTestId('once')); + expect(getByTestId('once')).toContainHTML(mockData1.a); + + await act(() => ref.set(mockData2)); + expect(getByTestId('once')).toContainHTML(mockData1.a); + }); + }); + describe('useFirestoreCollection', () => { it('can get a Firestore collection [TEST REQUIRES EMULATOR]', async () => { const mockData1 = { a: 'hello' }; diff --git a/reactfire/firestore/index.tsx b/reactfire/firestore/index.tsx index 1da73f0e..0b3e7d9a 100644 --- a/reactfire/firestore/index.tsx +++ b/reactfire/firestore/index.tsx @@ -13,6 +13,7 @@ import { checkStartWithValue } from '..'; import { preloadObservable } from '../useObservable'; +import { first } from 'rxjs/operators'; // starts a request for a firestore doc. // imports the firestore SDK automatically @@ -52,6 +53,23 @@ export function useFirestoreDoc( ); } +/** + * Get a firestore document and don't subscribe to changes + * + * @param ref - Reference to the document you want to get + * @param options + */ +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) + ); +} + /** * Suscribe to Firestore Document changes * @@ -70,6 +88,24 @@ export function useFirestoreDocData( ); } +/** + * Get a firestore document and don't subscribe to changes + * + * @param ref - Reference to the document you want to get + * @param options + */ +export function useFirestoreDocDataOnce( + ref: firestore.DocumentReference, + options?: ReactFireOptions +): T { + const idField = checkIdField(options); + return useObservable( + docData(ref, idField).pipe(first()), + `firestore:docDataOnce:${ref.firestore.app.name}:${ref.path}:idField=${idField}`, + checkStartWithValue(options) + ); +} + /** * Subscribe to a Firestore collection * @@ -87,7 +123,7 @@ export function useFirestoreCollection( return useObservable( fromCollectionRef(query), queryId, - options ? options.startWithValue : undefined + checkStartWithValue(options) ); } diff --git a/sample/src/Firestore.js b/sample/src/Firestore.js index 69748f27..282c641d 100644 --- a/sample/src/Firestore.js +++ b/sample/src/Firestore.js @@ -5,6 +5,7 @@ import { SuspenseWithPerf, useFirestoreCollectionData, useFirestoreDocData, + useFirestoreDocDataOnce, useFirestore } from 'reactfire'; @@ -32,6 +33,16 @@ const Counter = props => { ); }; +const StaticValue = props => { + const firestore = useFirestore(); + + const ref = firestore().doc('count/counter'); + + const { value } = useFirestoreDocDataOnce(ref); + + return {value}; +}; + const AnimalEntry = ({ saveAnimal }) => { const [text, setText] = useState(''); const [disabled, setDisabled] = useState(false); @@ -121,6 +132,14 @@ const SuspenseWrapper = props => { > +

Sample One-time Get

+ + + +

Sample Collection Listener