Marionette SPA to React/Redux
Most of my time at work is spent on a Marionette powered single page app. It transitioned from a traditionally served website about a year ago. We decided to keep our Javascript stack pretty much unchanged throughout the transition, deciding that our collective familiarity with it was too valuable to consider alternatives.
Unfortunately, Marionette has slowly become a little unwieldy as the app has grown and changed. I’d estimate half the bug reports that get filed are due to something not re-rendering when it should, or a full page render being triggered and wiping out some UI state.
I began experimenting with porting pieces of the app to React in my free time, and it quickly became obvious that Redux would work really well for us. React Router’s dynamic routing style was a no-brainer too. redux-auth-wrapper didn’t come in until later, but it’s such a brilliantly simple little module that it’s worth mentioning here.
Expressive is how I would sum up the benefit of this new stack; there’s nothing here that we couldn’t have done in Marionette, but it feels like a mountain of boilerplate code has disappeared since starting the transition.
I pitched the idea of porting the app to management and I was given the go-ahead to get started. After dropping my React code into the app and playing around with ways of making it play nice with the existing code base, I made a couple of decisions:
-
We wouldn’t bother hooking our Marionette views up to Redux. They were built for Backbone. The effort of morphing them into something different didn’t seem much less than the effort of rewriting them as React components entirely. Instead, they should be touched as little as possible and left to keep ticking along until they’re replaced.
-
The “core” of the app would be done with the new stack entirely, with Marionette views wrapped into React components. For our app, this meant a fairly large development overhead until everything was working again. This was mitigated by our use of Backbone.Radio to trigger things like modals, alerts, and route transitions - all things that were broken by chopping out the Marionette bootstrapping code. I just had to write new request handlers to call on the new stack as appropriate and they were working again.
The app is in a really nice place now. We’re reaping the benefits of React/Redux throughout and don’t have to give a second thought to the Marionette views. Wrapping them in a React component is simple:
export function wrapView(View, options = {}, callback = function () {}) {
return class WrappedView extends React.PureComponent {
static propTypes = {
match: RouterPropTypes.match
};
constructor(props) {
super(props);
this.viewOptions = isFunction(options)
? options(this.props.match, this.props)
: options;
this.view = new View(this.viewOptions);
}
componentDidMount() {
this.view.setElement(this.mnContainer);
this.view.render();
this.view.triggerMethod('attach');
callback(this.viewOptions);
}
componentWillUnmount() {
this.view.triggerMethod('before:destroy');
this.view.triggerMethod('destroy');
}
render() {
const { id, tagName, className = 'main-container' } = this.view;
return React.createElement(tagName, {
id,
className,
ref: el => (this.mnContainer = el)
});
}
};
}
You simply pass in your view, its options, and an optional callback. Options can be expressed as an object or as a function, in which case it’ll be passed the React Router match object first and the other props second (since most of the time you’ll be doing something like extracting an ID from the URL to pass to a model).
For views which made use of marionette-routing to display child views, for example tabbed content, we had to split them up into their constituent parts and put them in a React Router powered component. There’s probably a less involved way of doing this - maybe using a React portal to make the outlet
region visible to the new router would work.
I hope this post is helpful to anyone considering a similar upgrade to their Marionette SPA.