UNPKG

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