UNPKG

21.8 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{ collection: 'cities', doc: 'SF', subcollections: [{ collection: 'zipcodes' }] },
279// or string equivalent
280// props.store.firestore.get('cities/SF'/zipcodes),
281```
282
283**Note:** When nesting sub-collections, [`storeAs`](#storeas) should be used for more optimal state updates.
284
285##### Where
286
287To create a single `where` call, pass a single argument array to the `where` parameter:
288
289```js
290{
291 collection: 'cities',
292 where: ['state', '==', 'CA']
293},
294```
295
296Multiple `where` queries are as simple as passing multiple argument arrays (each one representing a `where` call):
297
298```js
299{
300 collection: 'cities',
301 where: [
302 ['state', '==', 'CA'],
303 ['population', '<', 100000]
304 ]
305},
306```
307
308Firestore doesn't alow you to create `or` style queries. Instead, you should pass in multiple queries and compose your data.
309
310``` javascript
311['sally', 'john', 'peter'].map(friendId => ({
312 collection: 'users',
313 where: [
314 ['id', '==', friendId],
315 ['isOnline', '==', true]
316 ]
317 storeAs: 'onlineFriends'
318}));
319```
320
321Since the results must be composed, a query like this is unable to be properly ordered. The results should be pulled from `data`.
322
323*Can only be used with collections*
324
325##### orderBy
326
327To create a single `orderBy` call, pass a single argument array to `orderBy`
328
329```js
330{
331 collection: 'cities',
332 orderBy: ['state'],
333 // orderBy: 'state' // string notation can also be used
334},
335```
336
337Multiple `orderBy`s are as simple as passing multiple argument arrays (each one representing a `orderBy` call)
338
339```js
340{
341 collection: 'cities',
342 orderBy: [
343 ['state'],
344 ['population', 'desc']
345 ]
346},
347```
348
349*Can only be used with collections*
350
351##### limit
352
353Limit the query to a certain number of results
354
355```js
356{
357 collection: 'cities',
358 limit: 10
359},
360```
361
362*Can only be used with collections*
363
364##### startAt
365
366> Creates a new query where the results start at the provided document (inclusive)
367
368[From Firebase's `startAt` docs](https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference#startAt)
369
370```js
371{
372 collection: 'cities',
373 orderBy: 'population',
374 startAt: 1000000
375},
376```
377
378*Can only be used with collections*
379
380##### startAfter
381
382> Creates a new query where the results start after the provided document (exclusive)...
383
384[From Firebase's `startAfter` docs](https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference#startAfter)
385
386```js
387{
388 collection: 'cities',
389 orderBy: 'population',
390 startAfter: 1000000
391},
392```
393
394*Can only be used with collections*
395
396##### endAt
397
398> Creates a new query where the results end at the provided document (inclusive)...
399
400[From Firebase's `endAt` docs](https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference#endAt)
401
402
403```js
404{
405 collection: 'cities',
406 orderBy: 'population',
407 endAt: 1000000
408},
409```
410
411*Can only be used with collections*
412
413##### endBefore
414
415> Creates a new query where the results end before the provided document (exclusive) ...
416
417[From Firebase's `endBefore` docs](https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference#endBefore)
418
419
420```js
421{
422 collection: 'cities',
423 orderBy: 'population',
424 endBefore: 1000000
425},
426```
427
428*Can only be used with collections*
429
430##### storeAs
431
432Storing data under a different path within redux is as easy as passing the `storeAs` parameter to your query:
433
434```js
435{
436 collection: 'cities',
437 where: ['state', '==', 'CA'],
438 storeAs: 'caliCities' // store data in redux under this path instead of "cities"
439},
440```
441
442**NOTE:** Usage of `"/"` and `"."` within `storeAs` can cause unexpected behavior when attempting to retrieve from redux state
443
444
445#### Other Firebase Statics
446
447Other Firebase statics (such as [FieldValue](https://firebase.google.com/docs/reference/js/firebase.firestore.FieldValue)) are available through the firestore instance:
448
449```js
450import PropTypes from 'prop-types'
451import { connect } from 'react-redux'
452import {
453 compose,
454 withHandlers,
455 withContext,
456 getContext
457} from 'recompose'
458
459const withStore = compose(
460 withContext({ store: PropTypes.object }, () => {}),
461 getContext({ store: PropTypes.object }),
462)
463
464const enhance = compose(
465 withStore,
466 withHandlers({
467 onDoneClick: props => (key, done = true) => {
468 const { firestore } = props.store
469 return firestore.update(`todos/${key}`, {
470 done,
471 updatedAt: firestore.FieldValue.serverTimestamp() // use static from firestore instance
472 }),
473 }
474 })
475)
476
477export default enhance(SomeComponent)
478```
479
480### Population
481Population, made popular in [react-redux-firebase](http://react-redux-firebase.com/docs/recipes/populate.html), also works with firestore.
482
483
484#### Automatic Listeners
485```js
486import { connect } from 'react-redux'
487import { firestoreConnect, populate } from 'react-redux-firebase'
488import {
489 compose,
490 withHandlers,
491 lifecycle,
492 withContext,
493 getContext
494} from 'recompose'
495
496const populates = [{ child: 'createdBy', root: 'users' }]
497const collection = 'projects'
498
499const withPopulatedProjects = compose(
500 firestoreConnect((props) => [
501 {
502 collection,
503 populates
504 }
505 ]),
506 connect((state, props) => ({
507 projects: populate(state.firestore, collection, populates)
508 }))
509)
510```
511
512#### Manually using setListeners
513```js
514import { withFirestore, populate } from 'react-redux-firebase'
515import { connect } from 'react-redux'
516import { compose, lifecycle } from 'recompose'
517
518const collection = 'projects'
519const populates = [{ child: 'createdBy', root: 'users' }]
520
521const enhance = compose(
522 withFirestore,
523 lifecycle({
524 componentDidMount() {
525 this.props.firestore.setListener({ collection, populates })
526 }
527 }),
528 connect(({ firestore }) => ({ // state.firestore
529 todos: firestore.ordered.todos,
530 }))
531)
532```
533
534#### Manually using get
535```js
536import { withFirestore, populate } from 'react-redux-firebase'
537import { connect } from 'react-redux'
538import { compose, lifecycle } from 'recompose'
539
540const collection = 'projects'
541const populates = [{ child: 'createdBy', root: 'users' }]
542
543const enhance = compose(
544 withFirestore,
545 lifecycle({
546 componentDidMount() {
547 this.props.store.firestore.get({ collection, populates })
548 }
549 }),
550 connect(({ firestore }) => ({ // state.firestore
551 todos: firestore.ordered.todos,
552 }))
553)
554```
555
556## Config Options
557Optional configuration options for redux-firestore, provided to reduxFirestore enhancer as optional second argument. Combine any of them together in an object.
558
559#### logListenerError
560Default: `true`
561
562Whether 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.
563
564#### enhancerNamespace
565Default: `'firestore'`
566
567Namespace under which enhancer places internal instance on redux store (i.e. `store.firestore`).
568
569#### allowMultipleListeners
570Default: `false`
571
572Whether 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.
573
574#### preserveOnDelete
575Default: `null`
576
577Values 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.
578
579#### preserveOnListenerError
580Default: `null`
581
582Values to preserve from state when LISTENER_ERROR action is dispatched.
583
584#### onAttemptCollectionDelete
585Default: `null`
586
587Arguments:`(queryOption, dispatch, firebase)`
588
589Function 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).
590
591#### mergeOrdered
592Default: `true`
593
594Whether or not to merge data within `orderedReducer`.
595
596#### mergeOrderedDocUpdate
597Default: `true`
598
599Whether or not to merge data from document listener updates within `orderedReducer`.
600
601
602#### mergeOrderedCollectionUpdates
603Default: `true`
604
605Whether or not to merge data from collection listener updates within `orderedReducer`.
606
607<!-- #### Middleware
608
609`redux-firestore`'s enhancer offers a new middleware setup that was not offered in `react-redux-firebase` (but will eventually make it `redux-firebase`)
610**Note**: This syntax is just a sample and is not currently released
611
612##### Setup
613```js
614```
615
616
617##### Usage
618
619```js
620import { FIREBASE_CALL } from 'redux-firestore'
621
622dispatch({
623 type: FIREBASE_CALL,
624 collection: 'users', // only used when namespace is firestore
625 method: 'get' // get method
626})
627```
628
629Some of the goals behind this approach include:
630
6311. Not needing to pass around a Firebase instance (with `react-redux-firebase` this meant using `firebaseConnect` HOC or `getFirebase`)
6322. Follows [patterns outlined in the redux docs for data fetching](http://redux.js.org/docs/advanced/ExampleRedditAPI.html)
6333. Easier to expand/change internal API as Firebase/Firestore API grows & changes -->
634
635## Builds
636
637Most 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.
638
639If 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.
640
641It can be imported like so:
642
643```html
644<script src="../node_modules/redux-firestore/dist/redux-firestore.min.js"></script>
645<!-- or through cdn: <script src="https://unpkg.com/redux-firestore@latest/dist/redux-firestore.min.js"></script> -->
646<script>console.log('redux firestore:', window.ReduxFirestore)</script>
647```
648
649Note: 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).
650
651## Applications Using This
652* [fireadmin.io](http://fireadmin.io) - Firebase Instance Management Tool (source [available here](https://github.com/prescottprue/fireadmin))
653
654## FAQ
6551. How do I update a document within a subcollection?
656
657 Provide `subcollections` config the same way you do while querying:
658
659 ```js
660 props.firestore.update(
661 {
662 collection: 'cities',
663 doc: 'SF',
664 subcollections: [{ collection: 'counties', doc: 'San Mateo' }],
665 },
666 { some: 'changes' }
667 );
668 ```
669
6701. How do I get auth state in redux?
671
672 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).
673
6741. Are there Higher Order Components for use with React?
675
676 [`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).
677
678## Roadmap
679
680* Automatic support for documents that have a parameter and a subcollection with the same name (currently requires `storeAs`)
681* Support for Passing a Ref to `setListener` in place of `queryConfig` object or string
682
683Post an issue with a feature suggestion if you have any ideas!
684
685[npm-image]: https://img.shields.io/npm/v/redux-firestore.svg?style=flat-square
686[npm-url]: https://npmjs.org/package/redux-firestore
687[npm-downloads-image]: https://img.shields.io/npm/dm/redux-firestore.svg?style=flat-square
688[quality-image]: http://npm.packagequality.com/shield/redux-firestore.svg?style=flat-square
689[quality-url]: https://packagequality.com/#?package=redux-firestore
690[travis-image]: https://img.shields.io/travis/prescottprue/redux-firestore/master.svg?style=flat-square
691[travis-url]: https://travis-ci.org/prescottprue/redux-firestore
692[daviddm-image]: https://img.shields.io/david/prescottprue/redux-firestore.svg?style=flat-square
693[daviddm-url]: https://david-dm.org/prescottprue/redux-firestore
694[climate-image]: https://img.shields.io/codeclimate/github/prescottprue/redux-firestore.svg?style=flat-square
695[climate-url]: https://codeclimate.com/github/prescottprue/redux-firestore
696[coverage-image]: https://img.shields.io/codecov/c/github/prescottprue/redux-firestore.svg?style=flat-square
697[coverage-url]: https://codecov.io/gh/prescottprue/redux-firestore
698[license-image]: https://img.shields.io/npm/l/redux-firestore.svg?style=flat-square
699[license-url]: https://github.com/prescottprue/redux-firestore/blob/master/LICENSE
700[code-style-image]: https://img.shields.io/badge/code%20style-airbnb-blue.svg?style=flat-square
701[code-style-url]: https://github.com/airbnb/javascript
702[gitter-image]: https://img.shields.io/gitter/room/redux-firestore/gitter.svg?style=flat-square
703[gitter-url]: https://gitter.im/redux-firestore/Lobby