Wrapping a Marionette app in React
Let's say your Marionette app looks something like the following:
import Marionette from "backbone.marionette";
import LayoutView from "./views/LayoutView";
export default Marionette.Application.extend({
// ...
onBeforeStart() {
this.layoutView = new LayoutView();
},
onStart() {
Backbone.history.start({
pushState: true
});
}
});
And
LayoutView
implicitly renders in a div#app
element:import { View } from "backbone.marionette";
export default View.extend({
el: "#app",
// ...
});
Chances are you will be willing to keep using this same element as the root of your brand new React app:
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Switch, Route, Link } from "react-router-dom";
import Wrapper from "./Wrapper";
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<nav>
<Link to="/">Marionette app</Link>
<Link to="/new-route">New route</Link>
</nav>
<Switch>
<Route path="/new-route">
<div>Haha React go brrr</div>
</Route>
<Route path="/">
<Wrapper />
</Route>
</Switch>
</BrowserRouter>
</React.StrictMode>,
document.getElementById("app")
);
The
Wrapper
component will therefore be responsible for the initialization and encapsulation of the Marionette app:import React, { useRef, useEffect } from "react";
import Backbone from "backbone";
import { useLocation } from "react-router-dom";
import MarionetteApp from "../backbone/app";
const app = new MarionetteApp();
export default function Wrapper() {
const wrapper = useRef(null);
useEffect(() => {
app.start({ el: wrapper.current });
// This will be called upon unmounting the component,
// i.e. navigating to a new, React-powered route
return () => {
app.destroy();
};
}, []);
// Ensure React links trigger Backbone navigation properly
useEffect(() => {
Backbone.history.navigate(location.pathname, { trigger: true });
}, [location.pathname]);
return (
<div ref={wrapper}></div>
);
}
Now, all that is left to do is to remove the
el
key from LayoutView
, and modify the Marionette app accordingly:import Marionette from "backbone.marionette";
import LayoutView from "./views/LayoutView";
export default Marionette.Application.extend({
// ...
onBeforeStart(app, options) {
this.layoutView = new LayoutView();
// Explicitly render inside the given node, i.e. our Wrapper component's
options.el.innerHTML = this.layout.render().$el;
},
// ...
onDestroy() {
this.layoutView.destroy();
Backbone.history.stop();
}
});
Implementing
onDestroy
as such ensures everything is cleaned up in order to start fresh when the user browses /
again and your Marionette app is remounted; it will not work otherwise.For this setup to be complete, you will also need to watch route changes on the Marionette side, in order to notify React Router accordingly. Your mileage may vary regarding how to do it; hopefully, you already are overriding
Backbone.Router
or Marionette.AppRouter
and emitting an event you can subscribe to. Regardless, here is what you want to do in the Wrapper
component:// ...
import { useLocation, withRouter } from "react-router-dom";
// ...
function Wrapper() {
const wrapper = useRef(null);
useEffect(() => {
app.start({ el: wrapper.current });
// Propagate Backbone route changes to React
someBackboneRadioChannelOrWhatever.on("navigationEvent", () => {
history.push("/" + Backbone.history.fragment);
});
return () => {
someBackboneRadioChannelOrWhatever.off("navigationEvent");
app.destroy();
};
}, []);
// ...
}
export default withRouter(Wrapper);