Meteor with Redux

Next time I build an app with a substantial front-end, I will manage the client state using Redux.

Redux is a tricky library. It can be at once simple and yet complicated. The author wrote it using straightforward JavaScript code, objects and functions and such. But it implements a lifecycle that spans many files, each accomplishing some small tick in the overall Redux flow.

Meteor doesn't provide a clearly defined technique to manage client state. What it does provide are ways to make sure that a selection of data on the client will exactly mirror that data on the server. And if the server data changes, Meteor allows you to 'subscribe' to the new data that the server 'publishes'.

You could use the client side Meteor Mongo collection to store your client state. Or you could hook into the Tracker events wherever possible and update the client Mongo accordingly.

Personally, I'm concerned about using Meteor with as few Meteor-specific techniques as possible, and leveraging popular libraries to implement important but specific tasks.

In this case, I would like to use Redux within a Meteor app the same way I would use it in a 'create-react-app' app.

In this example, we will build a vanilla, bare-bones Redux setup within a Meteor app. We will create only a couple actions and reducers. We will create redux-thunk and async actions that call Meteor methods.

See the completed example here

Create a new app

In your terminal:

$ meteor create meteor-with-redux-example

... computer does stuff...

$ cd meteor-with-redux-example

Once inside the new directory, let's install some NPM packages:

$ meteor npm install --save react react-dom redux react-redux redux-thunk

React and react-dom are standard here only because we are creating a react app. Redux doesn't actually depend on react, it is just very commonly used in react apps. But notice, we also install 'react-redux', a special package that adds react-specific features for use with Redux.

Redux-thunk allows async actions. We'll use it when we call Meteor methods.

Now, in your app, open the file '/client/main.js' and add some lines and remove the Blaze template stuff:

import React from 'react';  
import { render } from 'react-dom';  
import { Provider } from 'react-redux'  
import { createStore, applyMiddleware } from 'redux';  
import thunk from 'redux-thunk';


import './main.html';  
import appReducer from '../imports/ui/reducers'; // need to create  
import App from '../imports/ui/App'; // need to create

const store = createStore(appReducer, applyMiddleware(thunk));

Meteor.startup(() => {  
  render(
    <Provider store={store}>
      <App />
    </Provider>,
    document.getElementById('app')
  );
});


// /client/main.js

This sets the stage. We import all the react and redux packages, and 2 other forthcoming files: a root reducer and an App component.

We wrap our App component in react-redux's Provider component. This will take the 'store', the actual state of our app as produced by the reducer, and hand it as a prop to the App component.

We also need to revamp the main.html:

<head>  
  <title>meteor-with-redux-example</title>
</head>

<body>  
  <div id="app"></div>
</body>

<!-- /client/main.html -->  

Reducers

Let's build our reducers, first the root reducer:

import { combineReducers } from 'redux'  
import widgets from './widgets'

const appReducer = combineReducers({  
  widgets
});

export default appReducer


// /imports/ui/reducers/index.js

And next, the widgets reducer:

function widgets(state = [], action) {  
  switch (action.type) {
    case 'GET_WIDGETS':
      return action.payload;
    case 'CREATE_WIDGET':
      const { name, _id } = action.payload;

      return [
        ...state,
        { name, _id }
      ]
    default:
      return state
  }
}

export default widgets

// /imports/ui/reducers/widgets.js

Look at the widgets reducer first. This will handle just one node of our store's state, 'state.widgets'. It listens for some actions, GET_WIDGETS and CREATE_WIDGET, and produces a new 'state.widgets'.

The root reducer imports the widgets reducer. It also applies middleware as in redux-thunk. In between dispatching an action and reducing the result, Redux will execute middleware functions one after the other, ending at the reducer. In this case, we want to intercept actions and handle them async when appropriate.

App Component

Our reducers are set. Now, let's create our main App component, at '/imports/ui/App.js':

import React from 'react';  
import { bindActionCreators } from 'redux';  
import { connect } from 'react-redux';

import widgetActionCreators from './actions/widget-actions';

class App extends React.Component {

  componentDidMount() {
    const { widgetActions } = this.props;

    widgetActions.getWidgets();
  }

  handleForm(e) {
    e.preventDefault();
    const { widgetActions } = this.props;
    const name = this.refs.name.value;

    this.props.widgetActions.createWidget({ name });

    this.refs.name.value = '';
  }

  render() {
    const { widgets, widgetActions } = this.props;

    return (
      <div>
        <h1>widgets</h1>
        <ul>
          { widgets
            ?
            widgets.map(w => <li key={w._id}>{w.name}</li>)
            :
            <li>none</li>
          }
        </ul>
        <form onSubmit={this.handleForm.bind(this)}>
          <label htmlFor="name">Name</label>
          <input type="text" name="name" ref="name" />
          <input type="submit" value="Add Widget" />
        </form>
      </div>
    )
  }
}

function mapStateToProps(state) {  
  return {
    widgets: state.widgets
  }
}

function mapDispatchToProps(dispatch) {  
  return {
    widgetActions: bindActionCreators(widgetActionCreators, dispatch)
  }
}

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


// /imports/ui/App.js

This big file presents issues. In a real application, this should be a 'container' type component, responsible for fetching data and passing it along to child components. Here, we stick with one single component.

We will need to create some actions for this to work, but first glance at the connect function. React-redux gives us this function to pass along actions and state into the App component. mapStateToProps will take our state, mapDispatchToProps will take our actions, and all these will be set as props on the App.

Later, when the App component mounts, inside componentDidMount, we will dispatch the GET_WIDGETS action using this.props.widgetActions.getWidgets.

Also, when we fill the form and submit it, we will dispatch the CREATE_WIDGET action using this.props.widgetActions.createWidget.

Couple more things before we start testing.

Action Creators

We will create a single file to contain all widget-related actions. In '/imports/ui/actions/widget-actions':

import Widgets from '../../api/widgets';

function getWidgets() {  
  return async function(dispatch) {
    const widgets = await Meteor.callPromise('widgets.fetch');

    return dispatch({
      type: 'GET_WIDGETS',
      payload: widgets
    });
  }
}

function createWidget(data) {  
  return async function(dispatch) {
    const { name } = data;
    const widgetId = await Meteor.callPromise('widgets.insert', { name });

    return dispatch({
      type: 'CREATE_WIDGET',
      payload: {
        _id: widgetId,
        name
      }
    })
  }
}

export default {  
  createWidget,
  getWidgets
}

// /imports/ui/actions/widget-actions

This file exports 2 named functions, 'getWidgets' and 'createWidgets'. Each function itself returns a new function, this one async, that will execute some async logic and ultimately return a an 'action', which is a plain JavaScript object with 2 keys, 'type' and 'payload'. The object will be picked up by the appropriate reducer, based on the type.

Those actions will use Meteor methods, because of how Meteor works and because this file works on the client. Since we are using async / await, and we need Meteor methods to return promises, we will add the pack 'deanius:promise' so we can use Meteor.callPromise.

In the terminal, run:

$ meteor add deanius:promise

Go back to the Widgets collection and add two methods:

const Widgets = new Mongo.Collection('widgets');

Meteor.methods({  
  'widgets.fetch'() { return Widgets.find().fetch(); },
  'widgets.insert'(data) { return Widgets.insert(data); }
})

export default Widgets;

// /imports/api/widgets.js

At this point, our server does not know about the Widgets collection. We need to import it in '/server/main.js':

import { Meteor } from 'meteor/meteor';

import Widgets from '../imports/api/widgets';

Meteor.startup(() => {  
  // code to run on server at startup
});

// /server/main.js

Now we can test our app.

Working example

We can add widgets to our database. Everytime we load the page, React will dispatch our Redux action and fetch all the widgets. See the gif:

Creating and seeing our widgets

Now, of course, there is much more to do. We want to update widgets, delete widgets. We want to sign in users and display their private widgets. We want to build a complete app, buto our purpose here was simply to implement Redux within a Meteor app. We demonstrated how to dispatch a couple async actions that call some Meteor methods and reduce the results into the Redux store.

Going forward, we will want to balance our techniques between those that are specific to Meteor (using Tracker and MiniMongo) and with more general React and Redux techniques (listening for events, dispatching actions, updating the store.)

But the payoff could be great. Managing client state is tricky and Redux can help.

Cheers!

See the completed example here