Mar 29 2016

React and Redux a la carte

We were working with a client who was developing a brand new app. Like many startups, the client was interested in a lean approach at first and wanted to quickly create a proof of concept that would also entice early clients of their own. To develop quickly without sacrificing reliability, we chose to leverage Haml and Rails's ActionView to create a solid little app. The client was pleased. As they began user testing and sales meetings, the feature requests started coming in. After discussing these requests with the client we settled on the next stage of the app and what do you know, we needed JavaScript. Where to go next? So many options. We chose React and Redux as a promising combination.

One of the selling points of React is that the framework allows you to convert UI components into React as needed. You can take an existing app and upgrade a piece at a time. One of the selling points of the flux architecture of Redux is that you only ever have a single store of client side data. The challenge we were left with was to reconcile these two things: Add seemingly unrelated React components, all wired to a single Redux store, a la carte.

First approach

Our first approach was simple. Each page would essentially be its own React app with its own store. Initial data from the Rails controller would be bootstrapped in the Haml view, where the compiled JSX was included.

All JSX files would be required in a corresponding, plain JS file, which would, in turn, be precompiled by the Rails asset pipeline. A bit dirty, but it worked.

*A "container" is a Redux smart component. An "initializer" is the DOM element that a Provider attaches to.

// tasks/index.haml
# flash_initializer
...
# tasks_initializer
:javascript
globalData = {
flash = #{ JSON.parse(flash) },
tasks = #{ @tasks.map(&:serialize) }.map(JSON.parse)
}
= javascript_include_tag 'react/precompiled_tasks'
// tasks.js.jsx
let initialState = {
flash: globalData.flash,
tasks: globalData.tasks,
};
let reduxStore = createStore(reducer, initialState);
ReactDOM.render(
<Provider store={reduxStore}>
<FlashMessagesContainer />
</Provider>,
$("#flash_messages_initializer")[0],
);
ReactDOM.render(
<Provider store={reduxStore}>
<TasksContainer />
</Provider>,
$("#tasks_initializer")[0],
);
// precompiled_tasks.js
require './tasks'
// initializers/assets.rb
Rails.application.config.assets.precompile += %w(react / precompiled_tasks.js);

This scaled okay. As we added React to various pages, each was added to the precompiled assets list.

The difficulty came when we wanted to add a React component to the sidebar. The way to scale the current pattern was to now add a React app to every page that used the sidebar, over 20 pages. Already we were looking at repetitive declarations of the flash message component. Time to rethink this pattern.

A better approach

We solved this problem by moving all of the existing apps, (

tasks.js.jsx
, etc) into a single file,
app.js.jsx
, and conditionally wired each Redux Provider to a DOM element if that element exists on page load.
app.js.jsx
was then included on every page in the layout, via the same precompile technique.

(Note the difference in the

globalData
variable declaration.)

// layout.haml
:javascript
window.globalData = {}
= javascript_include_tag 'react/precompiled_app'
// tasks/index.haml
# tasks_initializer
:javascript
globalData.tasks = #{ @tasks.map(&:serialize) }.map(JSON.parse)
//app.js.jsx
let selector = '#flash_initializer'
if($(selector).length > 0) {
ReactDOM.render(
<Provider store={reduxStore}>
<FlashContainer />
</Provider>
$(selector)[0]
)
}
let selector = '#tasks_initializer'
if($(selector).length > 0) {
ReactDOM.render(
<Provider store={reduxStore}>
<TasksContainer />
</Provider>
$(selector)[0]
)
}

We took this one step further to DRY up the conditional logic.

function initializeComponent(selector, store, Component) {
if ($(selector).length > 0) {
ReactDOM.render(
<Provider store={store}>
<Component />
</Provider>,
$(selector)[0],
);
}
}
initializeComponent("#flash_initializer", reduxStore, function () {
return <FlashContainer />;
});
initializeComponent("#tasks_initializer", reduxStore, function () {
return <TasksContainer />;
});

Now we have a single React app, a single Redux store and a single client state structure for our entire Rails app. We've also eliminated the multiple provisions of the FlashContainer. At this point, adding a React/Redux component to the sidebar becomes as easy as 1-2-3.

  1. Bootstrap sidebar data onto the sidebar partial.
  2. Add any sidebar state to the initial state object.
  3. Add the sidebar container to
    app.js.jsx
    .
//_sidebar.haml
# sidebar_initializer
:javascript
globalData.currentUser = #{ current_user&.serialize }
// app.js.jsx
let initialState = {
flash: globalData.flash,
currentUser: globalData.currentUser,
tasks: globalData.tasks
}
let reduxStore = createStore(reducer, initialState);
...
initializeComponent('#sidebar_initializer', reduxStore, function() {
return <SidebarContainer />
})

Simple, Easy

By using a strategy that allowed us to easily add React components one at a time to a Rails app we have been able to reap a lot of React's benefits without having to commit to the more time-consuming strategy of full single page app. Best of all, by using this hybrid approach we've been able to quickly deliver a quality app to our client.

Happy coding, Adam Steel

P.S. We're ramping up our engineering team! We're hiring in Boulder and Providence. Apply now.

Adam Steel

Share: