Notable Pottery Manufacturer

    redux-firestore
    TypeScript icon, indicating that this package has built-in type declarations

    0.15.0 • Public • Published

    redux-firestore

    NPM version NPM downloads License Code Style Dependency Status Build Status Code Coverage

    Gitter

    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
    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 orderBys 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,
      })),
    );

    Config Options

    Optional configuration options for redux-firestore, provided to reduxFirestore enhancer as optional second argument. Combine any of them together in an object.

    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.

    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

    FAQ

    1. 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' },
      );
    2. 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.

    3. Are there Higher Order Components for use with React?

      react-redux-firebase contains firebaseConnect, firestoreConnect, withFirebase and withFirestore HOCs. For more information please visit the complementary package section.

    Roadmap

    • Automatic support for documents that have a parameter and a subcollection with the same name (currently requires storeAs)
    • Support for Passing a Ref to setListener in place of queryConfig object or string

    Post an issue with a feature suggestion if you have any ideas!

    Install

    npm i redux-firestore

    DownloadsWeekly Downloads

    5,819

    Version

    0.15.0

    License

    MIT

    Unpacked Size

    1 MB

    Total Files

    64

    Last publish

    Collaborators

    • prescottprue
    • reside-eng
    • fej-snikduj