redux-firestore
Redux bindings for Firestore. Provides low-level API used in other libraries such as react-redux-firebase
Installation
npm install redux-firestore --save
This assumes you are using npm as your package manager.
If you're not, you can access the library on unpkg, download it, or point your package manager to it. Theres more on this in the Builds section below
Complementary Package
Most likely, you'll want react bindings, for that you will need react-redux-firebase. You can install the current version it by running:
npm install --save react-redux-firebase
react-redux-firebase provides withFirestore
and firestoreConnect
higher order components, which handle automatically calling redux-firestore
internally based on component's lifecycle (i.e. mounting/un-mounting)
Use
import { createStore, combineReducers, compose } from 'redux'
import { reduxFirestore, firestoreReducer } from 'redux-firestore'
import firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/database'
import 'firebase/firestore'
const firebaseConfig = {} // from Firebase Console
// Initialize firebase instance
firebase.initializeApp(firebaseConfig)
// Initialize Cloud Firestore through Firebase
firebase.firestore();
// Add reduxFirestore store enhancer to store creator
const createStoreWithFirebase = compose(
reduxFirestore(firebase), // firebase instance as first argument
)(createStore)
// Add Firebase to reducers
const rootReducer = combineReducers({
firestore: firestoreReducer
})
// Create store with reducers and initial state
const initialState = {}
const store = createStoreWithFirebase(rootReducer, initialState)
Then pass store to your component's context using react-redux's Provider
:
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
render(
<Provider store={store}>
<MyRootComponent />
</Provider>,
rootEl
)
Call Firestore
Firestore Instance
Functional Components
It 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 by providing Higher Order Component functions such as withContext
, lifecycle
, and withHandlers
.
import { connect } from 'react-redux'
import {
compose,
withHandlers,
lifecycle,
withContext,
getContext
} from 'recompose'
const withStore = compose(
withContext({ store: PropTypes.object }, () => {}),
getContext({ store: PropTypes.object }),
)
const enhance = compose(
withStore,
withHandlers({
loadData: props => () => props.store.firestore.get('todos'),
onDoneClick: props => (key, done = false) =>
props.store.firestore.update(`todos/${key}`, { done }),
onNewSubmit: props => newTodo =>
props.store.firestore.add('todos', { ...newTodo, owner: 'Anonymous' }),
}),
lifecycle({
componentWillMount(props) {
props.loadData()
}
}),
connect(({ firebase }) => ({ // state.firebase
todos: firebase.ordered.todos,
}))
)
export default enhance(SomeComponent)
For more information on using recompose visit the docs
Component Class
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { watchEvents, unWatchEvents } from './actions/query'
import { getEventsFromInput, createCallable } from './utils'
class Todos extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
componentWillMount () {
const { firestore } = this.context.store
firestore.get('todos')
}
render () {
return (
<div>
{
todos.map(todo => (
<div key={todo.id}>
{JSON.stringify(todo)}
</div>
))
}
</div>
)
}
}
export default connect((state) => ({
todos: state.firestore.ordered.todos
}))(Todos)
API
The store.firestore
instance created by the reduxFirestore
enhancer extends Firebase's JS API for 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:
Actions
get
store.firestore.get({ collection: 'cities' }),
// store.firestore.get({ collection: 'cities', doc: 'SF' }), // doc
set
store.firestore.set({ collection: 'cities', doc: 'SF' }, { name: 'San Francisco' }),
add
store.firestore.add({ collection: 'cities' }, { name: 'Some Place' }),
update
const itemUpdates = {
some: 'value',
updatedAt: store.firestore.FieldValue.serverTimestamp()
}
store.firestore.update({ collection: 'cities', doc: 'SF' }, itemUpdates),
delete
store.firestore.delete({ collection: 'cities', doc: 'SF' }),
runTransaction
store.firestore.runTransaction(t => {
return t.get(cityRef)
.then(doc => {
// Add one person to the city population
const newPopulation = doc.data().population + 1;
t.update(cityRef, { population: newPopulation });
});
})
.then(result => {
// TRANSACTION_SUCCESS action dispatched
console.log('Transaction success!');
}).catch(err => {
// TRANSACTION_FAILURE action dispatched
console.log('Transaction failure:', err);
});
Types of Queries
get
props.store.firestore.get({ collection: 'cities' }),
// store.firestore.get({ collection: 'cities', doc: 'SF' }), // doc
onSnapshot/setListener
store.firestore.onSnapshot({ collection: 'cities' }),
// store.firestore.setListener({ collection: 'cities' }), // alias
// store.firestore.setListener({ collection: 'cities', doc: 'SF' }), // doc
setListeners
store.firestore.setListeners([
{ collection: 'cities' },
{ collection: 'users' },
]),
Query Options
Collection
{ collection: 'cities' },
// or string equivalent
// store.firestore.get('cities'),
Document
{ collection: 'cities', doc: 'SF' },
// or string equivalent
// props.store.firestore.get('cities/SF'),
Sub Collections
{ collection: 'cities', doc: 'SF', subcollections: [{ collection: 'zipcodes' }] },
// or string equivalent
// props.store.firestore.get('cities/SF'),
Note: When nesting sub-collections, storeAs
should be used for more optimal state updates.
Where
To create a single where
call, pass a single argument array to the where
parameter:
{
collection: 'cities',
where: ['state', '==', 'CA']
},
Multiple where
queries are as simple as passing multiple argument arrays (each one representing a where
call):
{
collection: 'cities',
where: [
['state', '==', 'CA'],
['population', '<', 100000]
]
},
Can only be used with collections
orderBy
To create a single orderBy
call, pass a single argument array to orderBy
{
collection: 'cities',
orderBy: ['state'],
// orderBy: 'state' // string notation can also be used
},
Multiple orderBy
s are as simple as passing multiple argument arrays (each one representing a orderBy
call)
{
collection: 'cities',
orderBy: [
['state'],
['population', 'desc']
]
},
Can only be used with collections
limit
Limit the query to a certain number of results
{
collection: 'cities',
limit: 10
},
Can only be used with collections
startAt
Creates a new query where the results start at the provided document (inclusive)
{
collection: 'cities',
orderBy: 'population',
startAt: 1000000
},
Can only be used with collections
startAfter
Creates a new query where the results start after the provided document (exclusive)...
From Firebase's startAfter
docs
{
collection: 'cities',
orderBy: 'population',
startAfter: 1000000
},
Can only be used with collections
endAt
Creates a new query where the results end at the provided document (inclusive)...
{
collection: 'cities',
orderBy: 'population',
endAt: 1000000
},
Can only be used with collections
endBefore
Creates a new query where the results end before the provided document (exclusive) ...
From Firebase's endBefore
docs
{
collection: 'cities',
orderBy: 'population',
endBefore: 1000000
},
Can only be used with collections
storeAs
Storing data under a different path within redux is as easy as passing the storeAs
parameter to your query:
{
collection: 'cities',
where: ['state', '==', 'CA'],
storeAs: 'caliCities' // store data in redux under this path instead of "cities"
},
NOTE: Usage of "/"
and "."
within storeAs
can cause unexpected behavior when attempting to retrieve from redux state
Other Firebase Statics
Other Firebase statics (such as FieldValue) are available through the firestore instance:
import { connect } from 'react-redux'
import {
compose,
withHandlers,
lifecycle,
withContext,
getContext
} from 'recompose'
const withFirestore = compose(
withContext({ store: PropTypes.object }, () => {}),
getContext({ store: PropTypes.object }),
)
const enhance = compose(
withStore,
withHandlers({
onDoneClick: props => (key, done = true) => {
const { firestore } = props.store
return firestore.update(`todos/${key}`, {
done,
updatedAt: firestore.FieldValue.serverTimestamp() // use static from firestore instance
}),
}
})
)
export default enhance(SomeComponent)
Config Options
logListenerError
Default: true
Whether 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.
enhancerNamespace
Default: 'firestore'
Namespace under which enhancer places internal instance on redux store (i.e. store.firestore
).
allowMultipleListeners
Default: false
Whether 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.
oneListenerPerPath
Default: false
If 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
preserveOnDelete
Default: null
Values 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.
preserveOnListenerError
Default: null
Values to preserve from state when LISTENER_ERROR action is dispatched.
onAttemptCollectionDelete
Default: null
Arguments:(queryOption, dispatch, firebase)
Function 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).
mergeOrdered
Default: true
Whether or not to merge data within orderedReducer
.
mergeOrderedDocUpdate
Default: true
Whether or not to merge data from document listener updates within orderedReducer
.
mergeOrderedCollectionUpdates
Default: true
Whether or not to merge data from collection listener updates within orderedReducer
.
Builds
Most commonly people consume Redux Firestore as a CommonJS module. This module is what you get when you import redux in a Webpack, Browserify, or a Node environment.
If you don't use a module bundler, it's also fine. The redux-firestore npm package includes precompiled production and development UMD builds in the dist folder. 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.
It can be imported like so:
<script src="../node_modules/redux-firestore/dist/redux-firestore.min.js"></script>
<!-- or through cdn: <script src="https://unpkg.com/redux-firestore@latest/dist/redux-firestore.min.js"></script> -->
<script>console.log('redux firestore:', window.ReduxFirestore)</script>
Note: In an effort to keep things simple, the wording from this explanation was modeled after the installation section of the Redux Docs.
Applications Using This
- fireadmin.io - Firebase Instance Management Tool (source available here)
FAQ
-
How do I update a document within a subcollection?
Provide
subcollections
config the same way you do while querying:props.firestore.update( { collection: 'cities', doc: 'SF', subcollections: [{ collection: 'counties', doc: 'San Mateo' }], }, { some: 'changes' } );
-
How do I get auth state in redux?
You will most likely want to use
react-redux-firebase
or another redux/firebase connector. For more information please visit the complementary package section. -
Are there Higher Order Components for use with React?
react-redux-firebase
containsfirebaseConnect
,firestoreConnect
,withFirebase
andwithFirestore
HOCs. For more information please visit the complementary package section.