3 min read

Controller and router: Cerebral, ReactJS and MeteorJS ES2015

In the previous controversial Post I've explained a bit the advantages of using Meteor as Client/Server data layer with mainstream NPM packages, but the architecture isn't complete.

React is the View, but we still miss something important.
It's possible to use directly Meteor with React, but we still have to think about the Controller and the Router. Specially if we plan to have several components and several optional subscriptions to the stream the data.
Flux, redux, reflux, react-router, relay,.. I tried to look at all these projects around React but.. they didn't fully convinced me. Because I just wanted a Controller and a Router.

Full reload Meteor Cerebral
Cerebral and Meteor signals/actions after a full reload. You can also play back and forth with the Cerebral time machine to replay the previous app states (URL changes included) :)

The Controller: Cerebral

Christian Alfoni, the author of Cerebral did a great job, I recommend his videos to understand deeply what it does. From my point of view:

  • It decouples the React components from Meteor/Minimongo
  • It exposes via props the status of the application. So you don't need components and the nested components with their own state.
  • It allows to define sequential or async actions, to modify the application state.

The Router: Cerebral-router

It is just a router. In the sense that with a certain URL activates some steps, to recreate the expected state and a certain action can change the shown URL.

The application do not depend on the router to work, but you have it, and you can just restore a state when you need it.

in-page link Meteor Cerebral
Cerebral and Meteor signals/actions after an in-page link clicked. The same URL but some actions aren't needed.

Cerebral and Meteor practically

Here is the live demo (please try it on your PC) and source that wire all together. Some intesting parts, the base is alwais meteor-webpack-react.

app
├── actions
│   ├── incrementLimit.js
│   ├── insertPost.js
│   ├── setPage.js
│   └── setSearchText.js
├── collections
│   └── index.js
├── components
│   ├── App.css
│   └── App.jsx
├── controller.js
├── fixtures.js
├── main_client.js
└── main_server.js

React components

App is the components managed by Cerebral and its decorator, its children Home and ListData may inherit its props.

Home is just text, ListData use ReactMeteorData and:

  • it initializes/reads data via the meteor subscription and Cerebral store

  • it writes on the application state or Minimongo data, triggering signals with optional payload (e.g {filter:'5'}).

@Cerebral({
  currentPage: ['currentPage'],
  searchText: ['searchText'],
  limit: ['limit']
})
class App extends React.Component {
  render() {
    return (
      <div>
        <h1>Cerebral + Meteor Header (App layout)</h1>
        {
          (this.props.searchText) ?
            `Active filter: ${this.props.searchText}` : ''
        }
        {(this.props.currentPage === 'Home' &&
        <div>
          Hi I'm Home, I don't need any subscription here.<br />
          Go to <a onClick={() => {this.props.signals.listOpened()} }>List</a>
          Set just <a onClick={() => {this.props.signals.searchText({filter:'5'}) } }>filter 5</a>
        </div>)}
        {(this.props.currentPage === 'List'
          && <ListData {...this.props} /> )}
      </div>
    );
  }
}

Controller, signals / actions

// An action that mutate the state, imagine it
// like a Lego(tm) brick
function setPage (pageName) {
  return function activePage (input, state, output, services) {
    console.log('Active: '+ pageName);
    document.title = pageName; // it updates the title
    state.set('currentPage', pageName);
  }
}

export default setPage;
...
/// controller.signal('aSignalName', ...listOfActions);
controller.signal('homeOpened', setPage('Home') );
controller.signal('listOpened', setPage('List') );
// coming from in-page link in ListData
controller.signal('searchText', setSearchText );
// coming from router
controller.signal('listOpenedSearch', setSearchText, setPage('List') );
controller.signal('loadMore', incrementLimit() );
controller.signal('insertPost', insertPost );

A signal can be triggered from many different places in the app and it runs sequentially or async some actions that modify the app state.
The Meteor subscriptions rerun when some props they use are modified by a state change.

Router and init

...
Router(controller, {
  '/': 'homeOpened',
  '/list': 'listOpened',
  '/list/:filter': 'listOpenedSearch',
}).start();

// Attach to the DOM
Meteor.startup(() => {
  React.render(<Container controller={controller} app={App}/>,
    document.getElementById('root'));
});

The router is just a router, it maps '/something':'toSignal', it knows nothing about the view and it's completly equivalent triggering a signal via url or via click. You just pipe the actions you need to reach the desired state (of course, if the app is already loaded in the section you need you'll trigger less actions).