UNPKG

22.9 kBMarkdownView Raw
1# redux-firestore
2
3[![NPM version][npm-image]][npm-url]
4[![NPM downloads][npm-downloads-image]][npm-url]
5[![License][license-image]][license-url]
6[![Code Style][code-style-image]][code-style-url]
7[![Dependency Status][daviddm-image]][daviddm-url]
8[![Build Status][travis-image]][travis-url]
9[![Code Coverage][coverage-image]][coverage-url]
10
11[![Gitter][gitter-image]][gitter-url]
12
13<!-- [![Quality][quality-image]][quality-url] -->
14
15> Redux bindings for Firestore. Provides low-level API used in other libraries such as [react-redux-firebase](https://github.com/prescottprue/react-redux-firebase)
16
17## Installation
18
19```sh
20npm install redux-firestore --save
21```
22
23This assumes you are using [npm](https://www.npmjs.com/) as your package manager.
24
25If you're not, you can access the library on [unpkg](https://unpkg.com/redux-firestore@latest/dist/redux-firestore.min.js), download it, or point your package manager to it. Theres more on this in the [Builds section below](#builds)
26
27## Complementary Package
28
29Most likely, you'll want react bindings, for that you will need [react-redux-firebase](https://github.com/prescottprue/react-redux-firebase). You can install the current version it by running:
30
31```sh
32npm install --save react-redux-firebase
33```
34
35[react-redux-firebase](https://github.com/prescottprue/react-redux-firebase) provides [`withFirestore`](http://react-redux-firebase.com/docs/api/withFirestore.html) and [`firestoreConnect`](http://react-redux-firebase.com/docs/api/firestoreConnect.html) higher order components, which handle automatically calling `redux-firestore` internally based on component's lifecycle (i.e. mounting/un-mounting)
36
37## Use
38
39```javascript
40import { createStore, combineReducers, compose } from 'redux';
41import { reduxFirestore, firestoreReducer } from 'redux-firestore';
42import firebase from 'firebase/app';
43import 'firebase/auth';
44import 'firebase/database';
45import 'firebase/firestore';
46
47const firebaseConfig = {}; // from Firebase Console
48const rfConfig = {}; // optional redux-firestore Config Options
49
50// Initialize firebase instance
51firebase.initializeApp(firebaseConfig);
52// Initialize Cloud Firestore through Firebase
53firebase.firestore();
54
55// Add reduxFirestore store enhancer to store creator
56const createStoreWithFirebase = compose(
57 reduxFirestore(firebase, rfConfig), // firebase instance as first argument, rfConfig as optional second
58)(createStore);
59
60// Add Firebase to reducers
61const rootReducer = combineReducers({
62 firestore: firestoreReducer,
63});
64
65// Create store with reducers and initial state
66const initialState = {};
67const store = createStoreWithFirebase(rootReducer, initialState);
68```
69
70Then pass store to your component's context using [react-redux's `Provider`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#provider-store):
71
72```js
73import React from 'react';
74import { render } from 'react-dom';
75import { Provider } from 'react-redux';
76
77render(
78 <Provider store={store}>
79 <MyRootComponent />
80 </Provider>,
81 rootEl,
82);
83```
84
85### Call Firestore
86
87#### Firestore Instance
88
89##### Functional Components
90
91It is common to make react components "functional" meaning that the component is just a function instead of being a `class` which `extends React.Component`. This can be useful, but can limit usage of lifecycle hooks and other features of Component Classes. [`recompose` helps solve this](https://github.com/acdlite/recompose/blob/master/docs/API.md) by providing Higher Order Component functions such as `withContext`, `lifecycle`, and `withHandlers`.
92
93```js
94import { connect } from 'react-redux';
95import {
96 compose,
97 withHandlers,
98 lifecycle,
99 withContext,
100 getContext,
101} from 'recompose';
102
103const withStore = compose(
104 withContext({ store: PropTypes.object }, () => {}),
105 getContext({ store: PropTypes.object }),
106);
107
108const enhance = compose(
109 withStore,
110 withHandlers({
111 loadData: props => () => props.store.firestore.get('todos'),
112 onDoneClick: props => (key, done = false) =>
113 props.store.firestore.update(`todos/${key}`, { done }),
114 onNewSubmit: props => newTodo =>
115 props.store.firestore.add('todos', { ...newTodo, owner: 'Anonymous' }),
116 }),
117 lifecycle({
118 componentDidMount(props) {
119 props.loadData();
120 },
121 }),
122 connect(({ firebase }) => ({
123 // state.firebase
124 todos: firebase.ordered.todos,
125 })),
126);
127
128export default enhance(SomeComponent);
129```
130
131For more information [on using recompose visit the docs](https://github.com/acdlite/recompose/blob/master/docs/API.md)
132
133##### Component Class
134
135```js
136import React, { Component } from 'react';
137import PropTypes from 'prop-types';
138import { connect } from 'react-redux';
139import { watchEvents, unWatchEvents } from './actions/query';
140import { getEventsFromInput, createCallable } from './utils';
141
142class Todos extends Component {
143 static contextTypes = {
144 store: PropTypes.object.isRequired,
145 };
146
147 componentDidMount() {
148 const { firestore } = this.context.store;
149 firestore.get('todos');
150 }
151
152 render() {
153 return (
154 <div>
155 {todos.map(todo => (
156 <div key={todo.id}>{JSON.stringify(todo)}</div>
157 ))}
158 </div>
159 );
160 }
161}
162
163export default connect(state => ({
164 todos: state.firestore.ordered.todos,
165}))(Todos);
166```
167
168### API
169
170The `store.firestore` instance created by the `reduxFirestore` enhancer extends [Firebase's JS API for Firestore](https://firebase.google.com/docs/reference/js/firebase.firestore). This means all of the methods regularly available through `firebase.firestore()` and the statics available from `firebase.firestore` are available. Certain methods (such as `get`, `set`, and `onSnapshot`) have a different API since they have been extended with action dispatching. The methods which have dispatch actions are listed below:
171
172#### Actions
173
174##### get
175
176```js
177store.firestore.get({ collection: 'cities' }),
178// store.firestore.get({ collection: 'cities', doc: 'SF' }), // doc
179```
180
181##### set
182
183```js
184store.firestore.set({ collection: 'cities', doc: 'SF' }, { name: 'San Francisco' }),
185```
186
187##### add
188
189```js
190store.firestore.add({ collection: 'cities' }, { name: 'Some Place' }),
191```
192
193##### update
194
195```js
196const itemUpdates = {
197 some: 'value',
198 updatedAt: store.firestore.FieldValue.serverTimestamp()
199}
200
201store.firestore.update({ collection: 'cities', doc: 'SF' }, itemUpdates),
202```
203
204##### delete
205
206```js
207store.firestore.delete({ collection: 'cities', doc: 'SF' }),
208```
209
210##### runTransaction
211
212```js
213store.firestore
214 .runTransaction(t => {
215 return t.get(cityRef).then(doc => {
216 // Add one person to the city population
217 const newPopulation = doc.data().population + 1;
218 t.update(cityRef, { population: newPopulation });
219 });
220 })
221 .then(result => {
222 // TRANSACTION_SUCCESS action dispatched
223 console.log('Transaction success!');
224 })
225 .catch(err => {
226 // TRANSACTION_FAILURE action dispatched
227 console.log('Transaction failure:', err);
228 });
229```
230
231#### Types of Queries
232
233Each of these functions take a queryOptions object with options as described in the [Query Options section of this README](#query-options). Some simple query options examples are used here for better comprehension.
234
235##### get
236
237```js
238props.store.firestore.get({ collection: 'cities' }),
239// store.firestore.get({ collection: 'cities', doc: 'SF' }), // doc
240```
241
242##### onSnapshot/setListener
243
244```js
245store.firestore.onSnapshot({ collection: 'cities' }),
246// store.firestore.setListener({ collection: 'cities' }), // alias
247// store.firestore.setListener({ collection: 'cities', doc: 'SF' }), // doc
248```
249
250##### setListeners
251
252```js
253store.firestore.setListeners([
254 { collection: 'cities' },
255 { collection: 'users' },
256]),
257```
258
259##### unsetListener / unsetListeners
260
261After setting a listener/multiple listeners, you can unset them with the following two functions. In order to unset a specific listener, you must pass the same queryOptions object given to onSnapshot/setListener(s).
262
263```js
264store.firestore.unsetListener({ collection: 'cities' }),
265// of for any number of listeners at once :
266store.firestore.unsetListeners([query1Options, query2Options]),
267// here query1Options as in { collection: 'cities' } for example
268```
269
270#### Query Options
271
272##### Collection
273
274```js
275{ collection: 'cities' },
276// or string equivalent
277// store.firestore.get('cities'),
278```
279
280##### Document
281
282```js
283{ collection: 'cities', doc: 'SF' },
284// or string equivalent
285// props.store.firestore.get('cities/SF'),
286```
287
288##### Sub Collections
289
290```js
291{
292 collection: 'cities',
293 doc: 'SF',
294 subcollections: [{ collection: 'zipcodes' }],
295 storeAs: 'SF-zipcodes' // make sure to include this
296},
297```
298
299**NOTE**: `storeAs` is now required for subcollections. This is to more closely match the logic of [the upcoming major release (v1)](https://github.com/prescottprue/redux-firestore/wiki/v1.0.0-Roadmap) which stores all collections, even subcollections, at the top level of `data` and `ordered` state slices.
300
301##### Collection Group
302
303```js
304{ collectionGroup: 'landmarks' },
305// does not support string equivalent
306```
307
308**Note:** When nesting sub-collections, [`storeAs`](#storeas) should be used for more optimal state updates.
309
310##### Where
311
312To create a single `where` call, pass a single argument array to the `where` parameter:
313
314```js
315{
316 collection: 'cities',
317 where: ['state', '==', 'CA']
318},
319```
320
321Multiple `where` queries are as simple as passing multiple argument arrays (each one representing a `where` call):
322
323```js
324{
325 collection: 'cities',
326 where: [
327 ['state', '==', 'CA'],
328 ['population', '<', 100000]
329 ]
330},
331```
332
333Firestore doesn't allow you to create `or` style queries. Instead, you should pass in multiple queries and compose your data.
334
335```javascript
336['sally', 'john', 'peter'].map(friendId => ({
337 collection: 'users',
338 where: [
339 ['id', '==', friendId],
340 ['isOnline', '==', true]
341 ]
342 storeAs: 'onlineFriends'
343}));
344```
345
346Since the results must be composed, a query like this is unable to be properly ordered. The results should be pulled from `data`.
347
348_Can only be used with collections_
349
350##### orderBy
351
352To create a single `orderBy` call, pass a single argument array to `orderBy`
353
354```js
355{
356 collection: 'cities',
357 orderBy: ['state'],
358 // orderBy: 'state' // string notation can also be used
359},
360```
361
362Multiple `orderBy`s are as simple as passing multiple argument arrays (each one representing a `orderBy` call)
363
364```js
365{
366 collection: 'cities',
367 orderBy: [
368 ['state'],
369 ['population', 'desc']
370 ]
371},
372```
373
374_Can only be used with collections_
375
376##### limit
377
378Limit the query to a certain number of results
379
380```js
381{
382 collection: 'cities',
383 limit: 10
384},
385```
386
387_Can only be used with collections_
388
389##### startAt
390
391> Creates a new query where the results start at the provided document (inclusive)
392
393[From Firebase's `startAt` docs](https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference#startAt)
394
395```js
396{
397 collection: 'cities',
398 orderBy: 'population',
399 startAt: 1000000
400},
401```
402
403_Can only be used with collections. Types can be a string, number, Date object, or an array of these types, but not a Firestore Document Snapshot_
404
405##### startAfter
406
407> Creates a new query where the results start after the provided document (exclusive)...
408
409[From Firebase's `startAfter` docs](https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference#startAfter)
410
411```js
412{
413 collection: 'cities',
414 orderBy: [['state', 'asc'],['population','desc']]
415 startAfter: ["CA", 1000000]
416},
417```
418
419**Note:** for the above to return valid results, there must be at least one document with `state = "CA"` _and_ `population = 1000000` (i.e. the values idenify "the provided document").
420
421_Can only be used with collections. Types can be a string, number, Date object, or an array of these types, but not a Firestore Document Snapshot_
422
423##### endAt
424
425> Creates a new query where the results end at the provided document (inclusive)...
426
427[From Firebase's `endAt` docs](https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference#endAt)
428
429```js
430{
431 collection: 'cities',
432 orderBy: 'population',
433 endAt: 1000000
434},
435```
436
437_Can only be used with collections. Types can be a string, number, Date object, or an array of these types, but not a Firestore Document Snapshot_
438
439##### endBefore
440
441> Creates a new query where the results end before the provided document (exclusive) ...
442
443[From Firebase's `endBefore` docs](https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference#endBefore)
444
445```js
446{
447 collection: 'cities',
448 orderBy: 'population',
449 endBefore: 1000000
450},
451```
452
453_Can only be used with collections. Types can be a string, number, Date object, or an array of these types, but not a Firestore Document Snapshot_
454
455##### storeAs
456
457Storing data under a different path within redux is as easy as passing the `storeAs` parameter to your query:
458
459```js
460{
461 collection: 'cities',
462 where: ['state', '==', 'CA'],
463 storeAs: 'caliCities' // store data in redux under this path instead of "cities"
464},
465```
466
467**Note:** Usage of `"/"` and `"."` within `storeAs` can cause unexpected behavior when attempting to retrieve from redux state
468
469#### Other Firebase Statics
470
471Other Firebase statics (such as [FieldValue](https://firebase.google.com/docs/reference/js/firebase.firestore.FieldValue)) are available through the firestore instance:
472
473```js
474import PropTypes from 'prop-types'
475import { connect } from 'react-redux'
476import {
477 compose,
478 withHandlers,
479 withContext,
480 getContext
481} from 'recompose'
482
483const withStore = compose(
484 withContext({ store: PropTypes.object }, () => {}),
485 getContext({ store: PropTypes.object }),
486)
487
488const enhance = compose(
489 withStore,
490 withHandlers({
491 onDoneClick: props => (key, done = true) => {
492 const { firestore } = props.store
493 return firestore.update(`todos/${key}`, {
494 done,
495 updatedAt: firestore.FieldValue.serverTimestamp() // use static from firestore instance
496 }),
497 }
498 })
499)
500
501export default enhance(SomeComponent)
502```
503
504### Population
505
506Population, made popular in [react-redux-firebase](http://react-redux-firebase.com/docs/recipes/populate.html), also works with firestore.
507
508#### Automatic Listeners
509
510```js
511import { connect } from 'react-redux';
512import { firestoreConnect, populate } from 'react-redux-firebase';
513import {
514 compose,
515 withHandlers,
516 lifecycle,
517 withContext,
518 getContext,
519} from 'recompose';
520
521const populates = [{ child: 'createdBy', root: 'users' }];
522const collection = 'projects';
523
524const withPopulatedProjects = compose(
525 firestoreConnect(props => [
526 {
527 collection,
528 populates,
529 },
530 ]),
531 connect((state, props) => ({
532 projects: populate(state.firestore, collection, populates),
533 })),
534);
535```
536
537#### Manually using setListeners
538
539```js
540import { withFirestore, populate } from 'react-redux-firebase';
541import { connect } from 'react-redux';
542import { compose, lifecycle } from 'recompose';
543
544const collection = 'projects';
545const populates = [{ child: 'createdBy', root: 'users' }];
546
547const enhance = compose(
548 withFirestore,
549 lifecycle({
550 componentDidMount() {
551 this.props.firestore.setListener({ collection, populates });
552 },
553 }),
554 connect(({ firestore }) => ({
555 // state.firestore
556 todos: firestore.ordered.todos,
557 })),
558);
559```
560
561#### Manually using get
562
563```js
564import { withFirestore, populate } from 'react-redux-firebase';
565import { connect } from 'react-redux';
566import { compose, lifecycle } from 'recompose';
567
568const collection = 'projects';
569const populates = [{ child: 'createdBy', root: 'users' }];
570
571const enhance = compose(
572 withFirestore,
573 lifecycle({
574 componentDidMount() {
575 this.props.store.firestore.get({ collection, populates });
576 },
577 }),
578 connect(({ firestore }) => ({
579 // state.firestore
580 todos: firestore.ordered.todos,
581 })),
582);
583```
584
585## Config Options
586
587Optional configuration options for redux-firestore, provided to reduxFirestore enhancer as optional second argument. Combine any of them together in an object.
588
589#### logListenerError
590
591Default: `true`
592
593Whether or not to use `console.error` to log listener error objects. Errors from listeners are helpful to developers on multiple occasions including when index needs to be added.
594
595#### enhancerNamespace
596
597Default: `'firestore'`
598
599Namespace under which enhancer places internal instance on redux store (i.e. `store.firestore`).
600
601#### allowMultipleListeners
602
603Default: `false`
604
605Whether or not to allow multiple listeners to be attached for the same query. If a function is passed the arguments it receives are `listenerToAttach`, `currentListeners`, and the function should return a boolean.
606
607#### preserveOnDelete
608
609Default: `null`
610
611Values to preserve from state when DELETE_SUCCESS action is dispatched. Note that this will not prevent the LISTENER_RESPONSE action from removing items from state.ordered if you have a listener attached.
612
613#### preserveOnListenerError
614
615Default: `null`
616
617Values to preserve from state when LISTENER_ERROR action is dispatched.
618
619#### onAttemptCollectionDelete
620
621Default: `null`
622
623Arguments:`(queryOption, dispatch, firebase)`
624
625Function run when attempting to delete a collection. If not provided (default) delete promise will be rejected with "Only documents can be deleted" unless. This is due to the fact that Collections can not be deleted from a client, it should instead be handled within a cloud function (which can be called by providing a promise to `onAttemptCollectionDelete` that calls the cloud function).
626
627#### mergeOrdered
628
629Default: `true`
630
631Whether or not to merge data within `orderedReducer`.
632
633#### mergeOrderedDocUpdate
634
635Default: `true`
636
637Whether or not to merge data from document listener updates within `orderedReducer`.
638
639#### mergeOrderedCollectionUpdates
640
641Default: `true`
642
643Whether or not to merge data from collection listener updates within `orderedReducer`.
644
645<!-- #### Middleware
646
647`redux-firestore`'s enhancer offers a new middleware setup that was not offered in `react-redux-firebase` (but will eventually make it `redux-firebase`)
648**Note**: This syntax is just a sample and is not currently released
649
650##### Setup
651```js
652```
653
654
655##### Usage
656
657```js
658import { FIREBASE_CALL } from 'redux-firestore'
659
660dispatch({
661 type: FIREBASE_CALL,
662 collection: 'users', // only used when namespace is firestore
663 method: 'get' // get method
664})
665```
666
667Some of the goals behind this approach include:
668
6691. Not needing to pass around a Firebase instance (with `react-redux-firebase` this meant using `firebaseConnect` HOC or `getFirebase`)
6702. Follows [patterns outlined in the redux docs for data fetching](http://redux.js.org/docs/advanced/ExampleRedditAPI.html)
6713. Easier to expand/change internal API as Firebase/Firestore API grows & changes -->
672
673## Builds
674
675Most commonly people consume Redux Firestore as a [CommonJS module](http://webpack.github.io/docs/commonjs.html). This module is what you get when you import redux in a Webpack, Browserify, or a Node environment.
676
677If you don't use a module bundler, it's also fine. The redux-firestore npm package includes precompiled production and development [UMD builds](https://github.com/umdjs/umd) in the [dist folder](https://unpkg.com/redux-firestore@latest/dist/). They can be used directly without a bundler and are thus compatible with many popular JavaScript module loaders and environments. For example, you can drop a UMD build as a `<script>` tag on the page. The UMD builds make Redux Firestore available as a `window.ReduxFirestore` global variable.
678
679It can be imported like so:
680
681```html
682<script src="../node_modules/redux-firestore/dist/redux-firestore.min.js"></script>
683<!-- or through cdn: <script src="https://unpkg.com/redux-firestore@latest/dist/redux-firestore.min.js"></script> -->
684<script>
685 console.log('redux firestore:', window.ReduxFirestore);
686</script>
687```
688
689**Note:** In an effort to keep things simple, the wording from this explanation was modeled after [the installation section of the Redux Docs](https://redux.js.org/#installation).
690
691## Applications Using This
692
693- [fireadmin.io](http://fireadmin.io) - Firebase Instance Management Tool (source [available here](https://github.com/prescottprue/fireadmin))
694
695## FAQ
696
6971. How do I update a document within a subcollection?
698
699 Provide `subcollections` config the same way you do while querying:
700
701 ```js
702 props.firestore.update(
703 {
704 collection: 'cities',
705 doc: 'SF',
706 subcollections: [{ collection: 'counties', doc: 'San Mateo' }],
707 },
708 { some: 'changes' },
709 );
710 ```
711
7121. How do I get auth state in redux?
713
714 You will most likely want to use [`react-redux-firebase`](https://github.com/prescottprue/react-redux-firebase) or another redux/firebase connector. For more information please visit the [complementary package section](#complementary-package).
715
7161. Are there Higher Order Components for use with React?
717
718 [`react-redux-firebase`](https://github.com/prescottprue/react-redux-firebase) contains `firebaseConnect`, `firestoreConnect`, `withFirebase` and `withFirestore` HOCs. For more information please visit the [complementary package section](#complementary-package).
719
720## Roadmap
721
722- Automatic support for documents that have a parameter and a subcollection with the same name (currently requires `storeAs`)
723- Support for Passing a Ref to `setListener` in place of `queryConfig` object or string
724
725Post an issue with a feature suggestion if you have any ideas!
726
727[npm-image]: https://img.shields.io/npm/v/redux-firestore.svg?style=flat-square
728[npm-url]: https://npmjs.org/package/redux-firestore
729[npm-downloads-image]: https://img.shields.io/npm/dm/redux-firestore.svg?style=flat-square
730[quality-image]: http://npm.packagequality.com/shield/redux-firestore.svg?style=flat-square
731[quality-url]: https://packagequality.com/#?package=redux-firestore
732[travis-image]: https://img.shields.io/travis/prescottprue/redux-firestore/master.svg?style=flat-square
733[travis-url]: https://travis-ci.org/prescottprue/redux-firestore
734[daviddm-image]: https://img.shields.io/david/prescottprue/redux-firestore.svg?style=flat-square
735[daviddm-url]: https://david-dm.org/prescottprue/redux-firestore
736[climate-image]: https://img.shields.io/codeclimate/github/prescottprue/redux-firestore.svg?style=flat-square
737[climate-url]: https://codeclimate.com/github/prescottprue/redux-firestore
738[coverage-image]: https://img.shields.io/codecov/c/github/prescottprue/redux-firestore.svg?style=flat-square
739[coverage-url]: https://codecov.io/gh/prescottprue/redux-firestore
740[license-image]: https://img.shields.io/npm/l/redux-firestore.svg?style=flat-square
741[license-url]: https://github.com/prescottprue/redux-firestore/blob/master/LICENSE
742[code-style-image]: https://img.shields.io/badge/code%20style-airbnb-blue.svg?style=flat-square
743[code-style-url]: https://github.com/airbnb/javascript
744[gitter-image]: https://img.shields.io/gitter/room/redux-firestore/gitter.svg?style=flat-square
745[gitter-url]: https://gitter.im/redux-firestore/Lobby
746
\No newline at end of file