Migrate from Backbone Router to React Router
Current implementation with Backbone Router
Static routing
In my playground I’m using a salsa of Backbone and React for two reasons: real life projects are like this, a combination of old and new and I was just too lazy to rewrite everything in React.
I kept the old static Backbone routing implemented by LibraryRouter.js
import Backbone from 'backbone';
import HeaderView from 'views/HeaderView';
import LibraryView from 'views/LibraryView';
let LibraryRouter = Backbone.Router.extend({
routes: {
'books' : 'manageBooks',
'books/:bookUuid': 'manageCurrentReadingSession'
},
initialize: function () {
this.headerView = new HeaderView();
this.headerView.render();
this.libraryView = new LibraryView();
if('/' === window.location.pathname) {
this.manageBooks();
}
},
manageBooks: function () {
this.libraryView.manageBooks();
},
manageCurrentReadingSession: function (bookUuid) {
this.libraryView.manageCurrentReadingSession(bookUuid);
}
});
export default LibraryRouter;
and I loaded root React component from a Backbone view implemented in ReadingSessionsView.js
import Backbone from 'backbone';
import React from 'react';
import ReactDOM from 'react-dom';
import { render } from 'react-dom';
import CurrentReadingSessionComponent from 'components/CurrentReadingSessionComponent';
const ReadingSessionsView = Backbone.View.extend({
tagName: 'div',
initialize: function (bookUuid) {
this.bookUuid = bookUuid;
},
render: function () {
render(
<CurrentReadingSessionComponent bookUuid={this.bookUuid}/>,
this.el
);
return this;
},
remove() {
ReactDOM.unmountComponentAtNode(this.el);
Backbone.View.prototype.remove.call(this);
}
});
export default ReadingSessionsView;
Navigation
I had to initialize Backbone history first in Library.js, application’s entry point
import LibraryRouter from 'routers/LibraryRouter';
jQuery.i18n.properties({
name: 'Messages',
path: '/js/bundle/',
mode: 'map',
checkAvailableLanguages: true,
async: true,
callback: function() {
new LibraryRouter();
Backbone.history.start({
pushState: true,
});
}
});
and then I was able to use it in HeaderView.js
import _ from 'underscore';
import Backbone from 'backbone';
import templateHtml from 'text!templates/Header.html';
const HeaderView = Backbone.View.extend({
el: '#header-div',
template: _.template(templateHtml),
events: {
'click #books-link': 'manageBooks'
},
render: function () {
this.$el.html(this.template());
return this;
},
manageBooks: function (e) {
e.preventDefault();
Backbone.history.navigate('/books', {trigger: true});
}
});
export default HeaderView;
and in BookView.js
readBook: function (e) {
e.preventDefault();
history.push('/books/' + this.book.get('uuid'));
},
Use React Router instead of Backbone Router
In order to use React Router instead of Backbone Router I had to change the loading order because:
- React Router is using React components
- Routing takes place as your app is rendering
I followed React Router guides and the steps applied to my react branch are below.
Dependencies
I added react-router-dom and react-router-history. If my project had only React components, only the first dependency was needed. Since I still keep some Backbone code, I had to use the second one too but this makes things more interesting.
Dynamic Routing
The most dramatic changes were done in LibraryRouter.js that switched from static routing to dynamic routing
import React from 'react';
import {
Router,
Route,
Redirect,
Switch
} from 'react-router-dom';
import HeaderComponent from 'components/HeaderComponent';
import LibraryViewComponent from 'components/LibraryViewComponent';
import CurrentReadingSessionComponent from 'components/CurrentReadingSessionComponent';
import history from 'routers/History';
const LibraryRouter = () => (
<Router history={history}>
<div>
<div className="page-header">
<HeaderComponent/>
</div>
<div className="page-content">
<Switch>
<Route exact path="/books" component={LibraryViewComponent}/>
<Route path="/books/:uuid" component={ ({ match }) => (
<CurrentReadingSessionComponent bookUuid={match.params.uuid}/>
)}/>
{/*
Without Switch I saw the following warning in console:
Warning: You tried to redirect to the same route you're currently on: "/books"
*/}
<Redirect exact from="/" to="/books"/>
</Switch>
</div>
</div>
</Router>
);
export default LibraryRouter;
I’m using Router and not BrowserRouter as it is recommened in Basic Components because I need to use React Router History from Backbone code as well. I will get back to this a little bit later when I’ll discuss about navigation.
The root component of the application is now the Router and since now routing and rendering are done together, I had to change the html template, index.html, of the application from
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Library application</title>
<link rel="stylesheet" type="text/css" href="/css/library.css"/>
</head>
<body>
<div class="page-header">
<header id="header-div" class="header"></header>
</div>
<div class="page-content">
<div id="content-div" class="content"></div>
</div>
</body>
</html>
to
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Library application</title>
<link rel="stylesheet" type="text/css" href="/css/library.css"/>
</head>
<body>
<div id="app-div"></div>
</body>
</html>
and the application’s entry point, Library.js to
import React from 'react';
import { render } from 'react-dom';
import LibraryRouter from 'routers/LibraryRouter';
jQuery.i18n.properties({
name: 'Messages',
path: '/js/bundle/',
mode: 'map',
checkAvailableLanguages: true,
async: true,
callback: function() {
render(<LibraryRouter/>, document.getElementById('app-div'));
}
});
HeaderComponent should be displayed in all situations so it doesn’t need a Route component. Switch is not really needed in my case but without it the Redirect displayed the following warning in the console
Warning: You tried to redirect to the same route you're currently on: "/books"
even if I used the exact
attribute.
Loading order
When Backbone router was used, I loaded a React component from a Backbone view
import Backbone from 'backbone';
import React from 'react';
import ReactDOM from 'react-dom';
import { render } from 'react-dom';
import CurrentReadingSessionComponent from 'components/CurrentReadingSessionComponent';
const ReadingSessionsView = Backbone.View.extend({
tagName: 'div',
initialize: function (bookUuid) {
this.bookUuid = bookUuid;
},
render: function () {
render(
<CurrentReadingSessionComponent bookUuid={this.bookUuid}/>,
this.el
);
return this;
},
remove() {
ReactDOM.unmountComponentAtNode(this.el);
Backbone.View.prototype.remove.call(this);
}
});
export default ReadingSessionsView;
This is not needed anymore and actually I have to do the opposite now
import React from 'react';
import LibraryView from 'views/LibraryView';
class LibraryViewComponent extends React.Component {
render() {
return (
<div id="content-div" className="content"></div>
);
}
componentDidMount() {
const libraryView = new LibraryView();
libraryView.manageBooks();
}
}
export default LibraryViewComponent;
Navigation
I still have a Backbone view so I cannot use the Link component there. The rescue comes from React router history which is initialized in History.js
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
export default history;
and used in BookView.js
readBook: function (e) {
e.preventDefault();
history.push('/books/' + this.book.get('uuid'));
}
and in LibraryRouter.js where is passed as a custom history
property to the Router component
<Router history={history}>
Originally the header was implemented as a Backbone view but I wanted to try the Link component so I migrated it to a React component
import React from 'react';
import { Link } from 'react-router-dom';
function HeaderComponent () {
return (
<header id="header-div" className="header">
<div>
<Link to='/books'>
<img src="/img/logo.svg" alt="Book Library" className="img-logo"/>
</Link>
</div>
</header>
);
}
export default HeaderComponent;
Redux integration
I had only two places that needed changes for Redux integration. The first one is related with Store initialisation in LibraryRouter.js and is specific for each type of async flow used.
Redux thunk integration
import React from 'react';
import {
Router,
Route,
Redirect,
Switch
} from 'react-router-dom';
import {
createStore,
applyMiddleware
} from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk'
import { Provider } from 'react-redux';
import { currentReadingSessionReducer } from 'reducers/CurrentReadingSessionReducer';
import HeaderComponent from 'components/HeaderComponent';
import LibraryViewComponent from 'components/LibraryViewComponent';
import CurrentReadingSessionComponent from 'components/CurrentReadingSessionComponent';
import history from 'routers/History';
const LibraryRouter = function() {
const store = createStore(currentReadingSessionReducer, composeWithDevTools(applyMiddleware(thunk)));
return (
<Router history={history}>
<div>
<div className="page-header">
<HeaderComponent/>
</div>
<div className="page-content">
<Switch>
<Route exact path="/books" component={LibraryViewComponent}/>
<Route path="/books/:uuid" component={({match}) => (
<Provider store={store}>
<CurrentReadingSessionComponent bookUuid={match.params.uuid}/>
</Provider>
)}/>
{/*
Without Switch I saw the following warning in console:
Warning: You tried to redirect to the same route you're currently on: "/books"
*/}
<Redirect exact from="/" to="/books"/>
</Switch>
</div>
</div>
</Router>
);
}
export default LibraryRouter;
Redux saga integration
import React from 'react';
import {
Router,
Route,
Redirect,
Switch
} from 'react-router-dom';
import {
createStore,
applyMiddleware
} from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from 'sagas/RootSagas';
import { composeWithDevTools } from 'redux-devtools-extension';
import { Provider } from 'react-redux';
import { currentReadingSessionReducer } from 'reducers/CurrentReadingSessionReducer';
import HeaderComponent from 'components/HeaderComponent';
import LibraryViewComponent from 'components/LibraryViewComponent';
import CurrentReadingSessionComponent from 'components/CurrentReadingSessionComponent';
import history from 'routers/History';
const LibraryRouter = function() {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(currentReadingSessionReducer, composeWithDevTools(applyMiddleware(sagaMiddleware)));
sagaMiddleware.run(rootSaga);
return (
<Router history={history}>
<div>
<div className="page-header">
<HeaderComponent/>
</div>
<div className="page-content">
<Switch>
<Route exact path="/books" component={LibraryViewComponent}/>
<Route path="/books/:uuid" component={({match}) => (
<Provider store={store}>
<CurrentReadingSessionComponent bookUuid={match.params.uuid}/>
</Provider>
)}/>
{/*
Without Switch I saw the following warning in console:
Warning: You tried to redirect to the same route you're currently on: "/books"
*/}
<Redirect exact from="/" to="/books"/>
</Switch>
</div>
</div>
</Router>
);
};
export default LibraryRouter;
The second one is how CurrentReadingSessionComponent is listening for Store changes
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
...
class CurrentReadingSessionComponent extends React.Component {
...
}
const mapStateToProps = state => {
return state
};
export default withRouter(connect(mapStateToProps)(CurrentReadingSessionComponent));
Conclusion
My application is too small to see a real benefit from combining dynamic routing with rendering but even so I liked how I see in only one place what is rendered and when.