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:

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.