Meteor with Webpack, React and Redux in Practice

Gildas Garcia
Gildas GarciaNovember 27, 2015
#popular#js#react

Introduction

In case you haven't heard about those technologies yet, here are some links and a very brief introduction to them. This article's goal is not to explain those technologies individually but rather to provide some examples about how to make them work together.

  1. Meteor: https://www.meteor.com

Meteor has been my platform of choice to quickly develop real time web applications which can easily be compiled as mobile applications too, thanks to phonegap integration. For a very good introduction to Meteor development, please refer to the BulletProof Meteor from Kadira.

  1. React: https://facebook.github.io/react/

React has changed the way we architecture our client applications by providing a fast and predictable way to organize our code into components.

  1. Redux: http://redux.js.org/

Redux has made the Flux architecture a lot simpler with a predictable state container, thus providing a single source of truth for the whole app, simplifying the way we reason about it. If you don't use it yet for your React applications, I suggest you read its documentation and watch the egghead videos.

  1. Webpack: https://webpack.github.io/

Webpack has simplified the way we structure our code, making bundling a bliss.

The Meteor problem

I've been a big fan of the Meteor platform as it allowed me to build complex applications very quickly as a single developer. That doesn't mean it's a perfect tool. Let's review my personal pros and cons about it:

Pros

  • Publications and subscriptions mechanisms - Simple way to define security rules on those publications - DDP protocol allowing easy and performant clients synchronizations
  • Authentication mechanisms (login/pwd, oauth)
  • Isomorphic way to query data
  • Some really good isomorphic packages are available from the community

Cons

  • Uses its own package manager and make npm usage possible but difficult (either use ported packages which are often out of date, or use a package which import npm packages but doesn't allow you to use semver wildcards)
  • Uses its own building tool which is slower than webpack and is not as extensible
  • Forces you to use global variables and old school namespaces
  • es6 recent integration does not include import and export

MDG (the company behind Meteor) is aware of those problems and is working on it as seen on their forum here. Fortunately, some Meteor developers have worked on ways to improve this.

How can we use those technologies together

I'm aware of two projects trying to address the Meteor/Webpack problem:

  1. https://github.com/thereactivestack/kickstart by @benoit_tremblay
  2. https://github.com/jedwards1211/meteor-webpack-react by jedwards

I've decided to go with jedwards's project as it allows me to use npm the standard way. The project is more difficult to apprehend tough, especially for Meteor developers used to run some simple commands to get things working.

Using jedwards's project, you don't depend anymore on Meteor integration of React. You can use the last version of React as you would on a standard node application. You can use webpack client and server side, allowing easy standard testing using karma/jasmine or mocha, hot reload and all the niceties you're used to with webpack.

Redux

Making Meteor work with Redux is pretty easy. Let's begin with a simple example: authentication with Google.

Configure Meteor authentication

Proceed as usual with Meteor:

  • add the following Meteor packages: accounts-google, service-configuration

  • get the api key and secret from your google console and put them in your Meteor settings file (settings.json)

    {
      "google": {
        "clientId": "...",
        "secret": "..."
      }
    }
  • initialize the Meteor ServiceConfiguration at server startup from your settings in app/main_server.js.

    Meteor.startup(() => {
      if (Meteor.settings.google) {
        ServiceConfiguration.configurations.upsert(
          {
            service: "google",
          },
          {
            $set: Meteor.settings.google,
          }
        );
      }
    });

For those unfamiliar with Meteor, it exposes a `Meteor` global and all installed packages usually extend existing globals or add new ones (as stated in my *Cons* section before). Here, `ServiceConfiguration` is responsible for managing accounts services (Google, Facebook, etc.) configurations. Please refer to the [documentation](http://docs.meteor.com/#/full/meteor_loginwithexternalservice) for more details.

### Redux actions
Let's put all the authentication related actions in `./app/client/actions/auth.js`.

#### Redux action for google signin
Meteor expose the `loginWithGoogle` method which will take care of the oauth process with a popup.

``` js
export function loginWithGoogle() {
return () => {
Meteor.loginWithGoogle(err => {
if (err) {
	alert('Error while login with google');
}
});
};
}

No need to handle the successful result of the action here as the current user will be automatically available with Meteor.user() which is a reactive datasource and will be called later to populate the store with the user data.

Redux action to load the user

Make sure to install the Redux thunk-middleware for this.

export const USER_LOGGING_IN = "USER_LOGGING_IN";
export const USER_DATA = "USER_DATA";

export function loadUser() {
  return dispatch => {
    Tracker.autorun(() => {
      dispatch({
        type: "USER_LOGGING_IN",
        data: Meteor.loggingIn(),
      });
    });

    Tracker.autorun(() => {
      dispatch({
        type: "USER_DATA",
        data: Meteor.user(),
      });
    });
  };
}

Tracker is another globally exposed object of the Meteor framework: it is the dependency tracking mechanism that will rerun a computation (the function supplied to autorun) whenever the reactive data sources used inside the computation change. Learn more about it in the documentation.

As we are running the dispatch inside a Tracker.autorun, the USER_LOGGING_IN action will be dispatched whenever the result of the Meteor.loggingIn() computation change. The same will happen for Meteor.user().

Redux action to logout

export function logout() {
  return () => {
    Meteor.logout(err => {
      if (err) {
        alert("Error while login with google");
      }
    });
  };
}

As with Meteor.loginWithGoogle, there's no need to dispatch an action when logout is successful because it will change the currently logged user and the reactive datasource Meteor.user() used in the loadUser action will trigger a reevaluation of the function inside the Tracker.autorun, dispatching a USER_DATA action with its data property set to null.

Redux reducers

Let's make the data available to the Redux state with reducers. The authentication reducers is in ./app/client/reducers/auth.js:

import assign from "object-assign";
import { USER_LOGGING_IN, USER_DATA } from "../actions/auth";

export const initialState = {
  user: null,
  loggingIn: false,
};

export default function(state = initialState, action) {
  const { data, type } = action;

  switch (type) {
    case USER_DATA:
      return assign({}, state, {
        user: data,
      });

    case USER_LOGGING_IN:
      return assign({}, state, {
        loggingIn: data,
      });

    default:
      return state;
  }
}

This is a standard reducer, it gets the data from the action and put it in the state.

The root reducer is defined in ./app/client/reducers/index.js and will combines all our reducers. For more details about reducers, see here.

import { combineReducers } from "redux";
import { routerStateReducer as router } from "redux-router";
import auth from "./auth";

export default combineReducers({
  auth,
  router,
});

React

The user should be available everywhere in our application so let's ensure Meteor.user() is bound to the store right from the beginning. When using jedwards project, this will happen in the main_client.js file (webpack entry point for client code):

/* global Meteor */
import React from "react";
import { render } from "react-dom";

import Root from "./client/containers/Root";
import configureStore from "./client/store";
import { loadUser } from "./client/actions/auth";

const store = configureStore();

Meteor.startup(() => {
  render(<Root {...{ store }} />, document.getElementById("root"));

  store.dispatch(loadUser());
});

Note that the React rendering only happen once when Meteor has finished loading. This is needed as otherwise the html would not be ready. In jedwards project, the html file is at ./meteor_core/meteor.html.

This is store initialization in './client/store.js':

import { applyMiddleware, compose, createStore } from "redux";
import { reduxReactRouter } from "redux-router";
import thunkMiddleware from "redux-thunk";

const createHistory = require("history/lib/createBrowserHistory");

import rootReducer from "../reducers";
import routes from "../routes";

const middlewares = [thunkMiddleware];

const finalCreateStore = compose(
  applyMiddleware(...middlewares),
  reduxReactRouter({ createHistory, routes })
)(createStore);

export default function configureStore(initialState) {
  return finalCreateStore(rootReducer, initialState);
}

The Root container in ./app/client/containers/Root.js:

import React, { PropTypes } from "react";
import { Provider } from "react-redux";
import { ReduxRouter } from "redux-router";

const Root = ({ store }) => (
  <Provider {...{ store }}>
    <ReduxRouter />
  </Provider>
);

Root.propTypes = {
  store: PropTypes.object.isRequired,
};

export default Root;

The routes are defined in ./app/client/routes.js:

import React from "react";
import { Route, IndexRoute } from "react-router";

import App from "../containers/App";
import Home from "../containers/Home";

export default (
  <Route path="/" component={App}>
    <IndexRoute component={Home} />
  </Route>
);

The App container in ./app/client/containers/App.js. Containers are Smart components: they do more than Dumb components which just render contents, they trigger actions and are connected to the store. More details here.

import { connect } from "react-redux";
import { bindActionCreators } from "redux";

import { logout } from "../actions/auth";
import App from "../components/App";

function mapStateToProps(state) {
  return {
    user: state.auth.user,
    logginIn: state.auth.logginIn,
  };
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators({ logout }, dispatch);
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

And the App component which is a pure component:

import React, { PropTypes } from "react";
import Icon from "react-fa";

const App = ({ children, loggingIn, logout, title, user }) => {
  return (
    <div className="container-fluid">
      <div className="row">
        <div className="col-xs-12">
          <nav className="navbar navbar-fixed-top navbar-dark bg-primary">
            <a className="navbar-brand" href="#">
              {title}
            </a>
            <ul className="nav navbar-nav">
              <li className="nav-item">
                <a className="nav-link" href="#">
                  Home
                  <span className="sr-only">(current)</span>
                </a>
              </li>
            </ul>
            {user && (
              <ul className="nav navbar-nav pull-right">
                <li className="nav-item dropdown">
                  <a
                    aria-haspopup="true"
                    aria-expanded="false"
                    className="nav-link dropdown-toggle"
                    data-toggle="dropdown"
                    href="#"
                    role="button"
                  >
                    {user.profile.name}
                  </a>
                  <div className="dropdown-menu dropdown-menu-right">
                    <a className="dropdown-item" href="#" onClick={logout}>
                      Sign out
                    </a>
                  </div>
                </li>
              </ul>
            )}
          </nav>
        </div>
      </div>
      <div className="row">
        <div className="col-xs-12 main-content">
          {loggingIn && <Icon spin name="spinner" />}
          {!loggingIn && children}
        </div>
      </div>
    </div>
  );
};

App.propTypes = {
  children: PropTypes.node,
  loggingIn: PropTypes.bool,
  logout: PropTypes.func.isRequired,
  title: PropTypes.string,
  user: PropTypes.object,
};

App.defaultProps = {
  title: "Team Time Tracker",
};

export default App;

The Home container in ./app/client/containers/Home.js:

import { connect } from "react-redux";
import { bindActionCreators } from "redux";

import { loginWithGoogle } from "../actions/auth";
import Home from "../components/Home";

function mapStateToProps(state) {
  return {
    ...state,
  };
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(
    {
      loginWithGoogle,
    },
    dispatch
  );
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Home);

And the Home component:

import React, { PropTypes } from "react";
import Fa from "react-fa";

const Home = ({ loginWithGoogle }) => {
  return (
    <div className="row">
      <div className="col-xs-12">
        <div className="jumbotron">
          <h1>Please sign in</h1>
          <p>
            <button className="btn btn-primary" onClick={loginWithGoogle}>
              <Fa name="user" />
              Sign in with Google
            </button>
          </p>
        </div>
      </div>
    </div>
  );
};

Home.propTypes = {
  loginWithGoogle: PropTypes.func.isRequired,
};

export default Home;

Going further

We only scratched the surface about how to make Meteor and Redux work together here. I've been working on a real application using this method. This is a Trello like application customized to handle nutritional plannings and to provide nutritionist coaches a tool to coach their clients.

my-nutrition-screenshot

I have used Redux middlewares to handle Meteor subscriptions, collections operations and calls to remote methods.

The application code is available on this repository: https://github.com/djhi/my-nutrition. Feel free to browse, comment and advise on better solutions !

Did you like this article? Share it!