UNPKG

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