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
const rfConfig = {}; // optional redux-firestore Config Options
// 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, rfConfig), // firebase instance as first argument, rfConfig as optional second
)(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({
componentDidMount(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,
};
componentDidMount() {
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
Each of these functions take a queryOptions object with options as described in the Query Options section of this README. Some simple query options examples are used here for better comprehension.
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' },
]),
unsetListener / unsetListeners
After 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).
store.firestore.unsetListener({ collection: 'cities' }),
// of for any number of listeners at once :
store.firestore.unsetListeners([query1Options, query2Options]),
// here query1Options as in { collection: 'cities' } for example
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' }],
storeAs: 'SF-zipcodes' // make sure to include this
},
NOTE: storeAs
is now required for subcollections. This is to more closely match the logic of the upcoming major release (v1) which stores all collections, even subcollections, at the top level of data
and ordered
state slices.
Collection Group
{ collectionGroup: 'landmarks' },
// does not support string equivalent
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]
]
},
Firestore doesn't allow you to create or
style queries. Instead, you should pass in multiple queries and compose your data.
['sally', 'john', 'peter'].map(friendId => ({
collection: 'users',
where: [
['id', '==', friendId],
['isOnline', '==', true]
]
storeAs: 'onlineFriends'
}));
Since the results must be composed, a query like this is unable to be properly ordered. The results should be pulled from data
.
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)
From Firebase's startAt
docs
{
collection: 'cities',
orderBy: 'population',
startAt: 1000000
},
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
startAfter
Creates a new query where the results start after the provided document (exclusive)...
From Firebase's startAfter
docs
{
collection: 'cities',
orderBy: [['state', 'asc'],['population','desc']]
startAfter: ["CA", 1000000]
},
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").
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
endAt
Creates a new query where the results end at the provided document (inclusive)...
From Firebase's endAt
docs
{
collection: 'cities',
orderBy: 'population',
endAt: 1000000
},
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
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. Types can be a string, number, Date object, or an array of these types, but not a Firestore Document Snapshot
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 PropTypes from 'prop-types'
import { connect } from 'react-redux'
import {
compose,
withHandlers,
withContext,
getContext
} from 'recompose'
const withStore = 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)
Population
Population, made popular in react-redux-firebase, also works with firestore.
Automatic Listeners
import { connect } from 'react-redux';
import { firestoreConnect, populate } from 'react-redux-firebase';
import {
compose,
withHandlers,
lifecycle,
withContext,
getContext,
} from 'recompose';
const populates = [{ child: 'createdBy', root: 'users' }];
const collection = 'projects';
const withPopulatedProjects = compose(
firestoreConnect(props => [
{
collection,
populates,
},
]),
connect((state, props) => ({
projects: populate(state.firestore, collection, populates),
})),
);
Manually using setListeners
import { withFirestore, populate } from 'react-redux-firebase';
import { connect } from 'react-redux';
import { compose, lifecycle } from 'recompose';
const collection = 'projects';
const populates = [{ child: 'createdBy', root: 'users' }];
const enhance = compose(
withFirestore,
lifecycle({
componentDidMount() {
this.props.firestore.setListener({ collection, populates });
},
}),
connect(({ firestore }) => ({
// state.firestore
todos: firestore.ordered.todos,
})),
);
Manually using get
import { withFirestore, populate } from 'react-redux-firebase';
import { connect } from 'react-redux';
import { compose, lifecycle } from 'recompose';
const collection = 'projects';
const populates = [{ child: 'createdBy', root: 'users' }];
const enhance = compose(
withFirestore,
lifecycle({
componentDidMount() {
this.props.store.firestore.get({ collection, populates });
},
}),
connect(({ firestore }) => ({
// state.firestore
todos: firestore.ordered.todos,
})),
);