In this article, we'll figure out how to add components like a header or a footer, which are rendered on all pages without having to include the component seperately in each view. We'll also see how to redirect to a default route.


This is the third and final article in a series of articles, on how I perform routing in my React Single Page Applications, without using the ubiquitous React-Router. If you haven't already, you should read them first.

So far, we've seen how to render a specific React component when a particular route is navigated to, and how to get BlueJacket to perform the routing when a link is clicked instead of having the browser navigate to the new url.


Please Note: The code for this example app is on Github as 'BlueJacket Twitter Clone'. You can checkout each individual commit to follow along, with each article in the series.


Now in our example, both the /tweets stream page, and the /tweets/:tweetId page simply display only their own contents. A common requirement for pretty much all applications is to display a branding header on every page.

To do this, let's first create a header component. Create a file /src/views/Header/index.js with the following content:

/**
 * Header Component
 */

import React from 'react';

import './index.css';

class Header extends React.Component {
    render() {
        const component = (
            <div
                className="header">
                <a href="/"><h3>Twitter-Clone</h3></a>
            </div>
        );

        return component;
    }
};

export default Header;

Let's also add some CSS to make it look pretty. Create /src/views/Header/index.css:

.header h3 {
    font-weight: normal;
    text-align: center;
    border-bottom: 1px solid rgba(0, 0, 0, 0.25);
    padding-bottom: 1rem;
}

.header a,
.header a:visited {
    text-decoration: none;
    color: rgb(0, 0, 0);
}

Now that we've created the component, let's create a middleware method to inject it into the context.childList that get's rendered. Create a new file /src/routes/header.js with the following content:

/**
 * Header middleware handler
 */

import React from 'react';

import Header from '../views/Header';

async function addHeaderComponent(context) {
    context.addComponent(<Header key="page-header" />);
};

export default addHeaderComponent;

As you can see this is a bit different from how we define our previous route handlers. For one, we don't export a setup method. Instead we directly export the controller, which in this case is addHeaderComponent. The addHeaderComponent simply adds an instance of the Header component to the context.childList using the context.addComponent mixin, we defined in the first article.


Next, let's pull in the middleware in the /tweets and /tweet/:tweetId route handlers. Change /src/routes/stream.js to:

/**
 * Stream related routes
 */

import React from 'react';

import StreamListView from '../views/StreamList';
import addHeaderComponent from './header';

/**
 * Retrieves top 20 items from '/api/v1/:user/tweets'
 * And renders them here
 *
 * We'll assume that the user is always logged in and
 * their user id is always 'lestrade'.
 */
async function list(context) {
    const userId = 'lestrade';

    const response = await fetch(`http://localhost:4000/api/v1/${userId}/stream`);
    const streamItems = (await response.json()).data;

    function renderComponent(props) {
        context.addComponent(<StreamListView key="stream-list" {...props} />);
        context.render();
    };

    return renderComponent({
        tweets: streamItems
    });
};

function setup(router) {
    router.handle('/tweets', addHeaderComponent, list);
};

export default setup;

As you can see we've imported the addHeaderComponent method, and we are providing it as the second parameter to the router.handle call. router.handle accepts any number of arguments, with the first being an ExpressJS style route string/regex for it to handle, and the remaining being handlers that get called in series.

These handler's may be sync or async methods. If it is an async method, it should return a promise, and BlueJacket will wait till the promise is resolved before calling the next method.

This pattern should be extremely familiar to any one who has worked with ExpressJS and it's middleware.

Let's make similar changes to /src/routes/tweet.js:

/**
 * Tweet related routes
 */

import React from 'react';

import TweetView from '../views/Tweet';
import addHeaderComponent from './header';

/**
 * Retrieves a tweet and it's replies by tweetId and renders it
 */
async function get(context) {
    const response = await fetch(`http://localhost:4000/api/v1/tweet/${context.params.tweetId}`);
    const tweetData = (await response.json()).data;

    function renderComponent(props) {
        context.addComponent(<TweetView key={`tweet-${props.id}`} {...props} />);
        context.render();
    };

    return renderComponent({ ...tweetData });
};

function setup(router) {
    router.handle('/tweet/:tweetId', addHeaderComponent, get);
};

export default setup;

We're doing the exact same thing here, and providing addHeaderComponent as a middleware method to router.handle.

Now, let's run our application again, and navigate to /tweets.

Tweet Stream with Header

And clicking on a tweet takes us to the tweet details page, which also has the header:

Tweet Details with Header

Success!


But wait, what happens when you click on the header? Well, nothing happens. Why? Because it's a link to /, which is not one of the routes that we have registered using router.handle. That's also why, we need to explicitly type in /tweets to see the tweet stream page.

This is of course bad UX, and in a serious application you would want / to automatically redirect to /tweets. To implement this, lets register a route handler for /. Open up /src/routes/stream.js and change it to:

/**
 * Stream related routes
 */

import React from 'react';

import StreamListView from '../views/StreamList';
import addHeaderComponent from './header';

/**
 * Retrieves top 20 items from '/api/v1/:user/tweets'
 * And renders them here
 *
 * We'll assume that the user is always logged in and
 * their user id is always 'lestrade'.
 */
async function list(context) {
    const userId = 'lestrade';

    const response = await fetch(`http://localhost:4000/api/v1/${userId}/stream`);
    const streamItems = (await response.json()).data;

    function renderComponent(props) {
        context.addComponent(<StreamListView key="stream-list" {...props} />);
        context.render();
    };

    return renderComponent({
        tweets: streamItems
    });
};

function redirectToStreamList(router) {
    return function redirect(context) {
        router.resolve('/tweets');
    };
};

function setup(router) {
    router.handle('/', redirectToStreamList(router));
    router.handle('/tweets', addHeaderComponent, list);
};

export default setup;

Now, run the application, and click on the header, and it should automatically route you to the /tweets page.


And with that we have come to the end of our little series on routing in React single page applications, without using the ubiquitous React-Router. I hope you find this useful. Thank you for reading so far, and do let me know what you think. Write to me at asleepysamurai at google's mail service.