Skip to content

Migrating Marionette Components to React

Andrew Fiedler edited this page Feb 7, 2019 · 3 revisions

Introduction

A good chunk of the UI packages are currently written in Marionette(js). The versions used typically range from 1.8.8 to 2.47. Marionette is a framework that builds upon Backbone(js), particularly bolstering the base view architecture that Backbone comes with. So when you're wondering about views, checkout Marionette. When you're wondering about models, checkout Backbone.

Bare Minimum

I'm going to try and dive into the bare minimum you need to know about these two frameworks in order to get started on migrating to React.

Marionette has three core types of views that we utilize: Item, Layout, and Collection. You might be familiar with another called Composite, but we've done some work to make this view type unnecessary (and later versions of Marionette have removed it).

Item Views

This is the base view that the others extend from, so let's start here.

Item View Example 1

At their most basic they look like this:

const template = require('MyItemView.hbs'); // just how we do things, there are other ways

Marionette.ItemView.extend({
   template
});

If this view were to be used, it would render what's in the MyItemView.hbs handlebars file and nothing else. There's no eventing, no models, etc.

Let's say MyItemView.hbs looks like this:

<div>Hello World</div>

If we wanted to convert this simple Marionette view to React, we would first need to import React into the component file. Your IDE won't complain about this, but at runtime nothing will work if you don't. This is because JSX is just syntactic sugar for calls to React.

import * as React from 'react'
Marionette.ItemView.extend({
   template() {
      return <div>Hello World</div>
   }
})

That should be all that's needed to get started, so long as your application has the Marionette rendering patched to allow JSX as a template (at the moment, the UI under ui/packages/catalog-ui-search which we commonly refer to as Intrigue or the Catalog UI).

ItemView Example 2

Now let's imagine a view that's more complex, one that involves a model and a template that involves rendering data from that model:

const template = require('MyItemView.hbs');
const model = new Backbone.Model({
   title: 'This guide is okay.'
});

Marionette.ItemView.extend({
   template,
   model
});

Where MyItemView.hbs is:

<div>Title: {{title}}</div>

The conversion to React would then be:

import * as React from 'react'
const model = new Backbone.Model({
   title: 'This guide is okay.'
});

Marionette.ItemView.extend({
   template(props) {
      return <div>Title: {props.title}</div>
   },
   model
})

Item View Example 3

Let's get more complex and add some events.

const template = require('MyItemView.hbs');
const model = new Backbone.Model({
   title: 'This guide is okay.'
});

Marionette.ItemView.extend({
   events: {
      'click > div': 'onClick'
   },
   template,
   model,
   onClick(e) {
      alert('You clicked on the div')
   }
});

Where MyItemView.hbs is:

<div>Title: {{title}}</div>

In this case, clicking on the div will result in the onClick method running and an alert showing up.

The conversion to React would then be:

import * as React from 'react'
const model = new Backbone.Model({
   title: 'This guide is okay.'
});

Marionette.ItemView.extend({
   template(props) {
      return <div onClick={this.onClick}>Title: {props.title}</div>
   },
   model,
   onClick(e) {
      alert('You clicked on the div')
   }
})

Layout Views

Layout views are pretty much exactly the same as Item views with one big difference: they're meant to contain other views. Item views are seen as leafs, whereas layout views are seen as branches. Those pathways to other views are referred to as regions in Marionette. We'll go ahead and assume that you're familiar with the jsx style syntax for the templates going forward. If you aren't, please start with the Item view section.

Layout View Example 1

Note: React.Fragment allows us to avoid needing a wrapper element, look it up! If you're in a tsx file, you can use <></> instead.

import * as React from 'react'
const model = new Backbone.Model({
   title: 'This guide is okay.'
});

const OtherComponentDefinition = Marionette.ItemView.extend({
   template(props) {
      return <div>{props.title} is a terrible title because {this.options.anyOtherOption}</div>
   }
});

Marionette.LayoutView.extend({
   regions: {
      otherComponent: '> div.other'
   },
   model,
   template(props) {
      return (
        <React.Fragment>
           <div>Title: {props.title}</div>
           <div className="otherComponent"></div>
        </React.Fragment>
      )
   },
   onRender() { //could be onBeforeShow or elsewhere, depends on the functionality desired
      this.otherComponent.show(new OtherComponentDefinition({
         model: this.model,
         anyOtherOption: 'I said so'
      });
   }
})

While this would be fairly simple to convert to React wholesale, let's pretend OtherComponentDefinition as a component is something much more complex and that we don't want to bother converting it to React because we don't have time.

In this case, what we would do is pull in a helper component that allows us to essentially bail back out of React at any point. That component is called MarionetteRegionContainer (soon to be shortened to Marionette). As a result, our conversion will end up looking like this:

import * as React from 'react'
import MarionetteRegionContainer from 'someUniquePath/marionette-region-container'

const model = new Backbone.Model({
   title: 'This guide is okay.'
})

const OtherComponentDefinition = Marionette.ItemView.extend({
   template(props) {
      return <div>{props.title} is a terrible title because {this.options.anyOtherOption}</div>
   }
})

Marionette.LayoutView.extend({
   model,
   template(props) {
      return (
        <React.Fragment>
           <div>Title: {props.title}</div>
           <MarionetteRegionContainer 
              className='otherComponent'
              view={OtherComponentDefinition}
              viewOptions={{
                 model: this.model,
                 anyOtherOption: 'I said so'
              }}
           />
        </React.Fragment>
      )
   }
})

The MarionetteRegionContainer component takes care of interoperation between React and Marionette for you.

Using it will allow you to jump in at any point in the tree of components that make up our UI and start migrating things to React knowing with confidence that you can bail out at any moment and things will still work.

Collection Views

Collection views are meant to simply house Item views, so they're typically smaller. In short, they're used to render lists.

Collection View Example 1

import MarionetteRegionContainer from 'someUniquePath/marionette-region-container'

const collection = new Backbone.Collection([
   { title: 'First title' },
   { title: 'Second title'}
]);

const ItemViewDefinition = Marionette.ItemView.extend({
   template(props) {
      return <div>Title: {props.title}</div>
   }
})

const CollectionViewDefinition = Marionette.CollectionView.extend({
   collection,
   childView: ItemViewDefinition
})

Marionette.LayoutView.extend({
   template() {
      return (
         <React.Fragment>
            <div>Collection is below:</div>
            <MarionetteRegionContainer 
               view={CollectionViewDefinition}
               viewOptions={{
                  collection
               }}
            />
         </React.Fragment>
      )
   }
})

We'll show the conversion in two steps, just to illustrate what's possible.

import MarionetteRegionContainer from 'someUniquePath/marionette-region-container'

const collection = new Backbone.Collection([
   { title: 'First title' },
   { title: 'Second title'}
]);

const ItemViewDefinition = Marionette.ItemView.extend({
   template(props) {
      return <div>Title: {props.title}</div>
   }
})

Marionette.LayoutView.extend({
   template() {
      return (
         <React.Fragment>
            <div>Collection is below:</div>
            {collection.map((model) => {
               return <MarionetteRegionContainer 
                  view={ItemViewDefinition}
                  viewOptions={{
                     model
                  }}
               />
            }}
         </React.Fragment>
      )
   }
})

But given that the ItemViewDefinition isn't as complex, we can go further here:

import MarionetteRegionContainer from 'someUniquePath/marionette-region-container'

const collection = new Backbone.Collection([
   { title: 'First title' },
   { title: 'Second title'}
]);

const ItemViewDefinition = (props) => {
   const { title } = props
   return <div>Title: {model.get('title')}</div>
}

Marionette.LayoutView.extend({
   template() {
      return (
         <React.Fragment>
            <div>Collection is below:</div>
            {collection.map((model) => {
               const props = model.toJSON()
               return <ItemViewDefinition ...props />
            }}
         </React.Fragment>
      )
   }
})

Or, since the ItemViewDefinition isn't too complex, we could choose to inline the component.

import MarionetteRegionContainer from 'someUniquePath/marionette-region-container'

const collection = new Backbone.Collection([
   { title: 'First title' },
   { title: 'Second title'}
]);

Marionette.LayoutView.extend({
   template() {
      return (
         <React.Fragment>
            <div>Collection is below:</div>
            {collection.map((model) => {
               const props = model.toJSON()
               return <div>Title: {props.title}</div>
            }}
         </React.Fragment>
      )
   }
})

This wraps up the basics for how to start converting a Marionette component to React. They can get a lot more complex, but the good news is you can bite off little chunks at a time thanks to being able to bail back out to Marionette.

What if I need Backbone in my React?

Sometimes we can't just toJSON a model and pass it to a React component because the React component needs to be able to respond to changes in the model. In this case we can rely on a special component that's a higher order component (HOC). The HOC will pass in special prop that mirror those available on Marionette / Backbone components and work the same way. These props are: listenTo, stopListening and listenToOnce. Here is the documentation. The component itself is called backbone-container, although it's exported with the name withListenTo to hint at the fact it's a HOC.

Let's dive into an example to get a better understanding

React with Backbone Example 1

import withListenTo from 'someUniquePath/backbone-container'
const model = new Backbone.model({
    currentTitle: 'Someone selected this'
})

const mapBackboneToState = () => {
   return model.toJSON()
} 

class MyComponent extends React.Component {
   constructor(props) {
      super(props)
      this.state = mapBackboneToState()
      this.props.listenTo(model, 'change', this.updateState)
   }
   updateState = () => {
      this.setState(mapBackboneToState())
   }
   render() {
      const { currentTitle } = this.state
      return <div> Title: {currentTitle}</div>
   }
}
export default withListenTo(MyComponent)

No need to worry about lifecycles or having to manually unlisten. The HOC takes care of cleaning up listeners for you just as Backbone or Marionette components would. Just remember to wrap your component before exporting it!

Summary

That's basically all the information you'll need to get started. The components you encounter are likely to be more complex and possibly use other Backbone / Marionette functionality (such as serializeData), so don't be afraid to look at the documentation for the frameworks or ask someone for help.