Building a React-based Application


Building a frontend application is hard. Compared to building backend applications, you have way too much mutable state to manage and in the end your whole code base is so complex that nobody wants to touch anything.

We already know how to build backend applications - generating html, sending it to the browser, repeat - without it ending in a nightmare of complexity, but building frontend stuff is very complicated. Backend is easy, frontend is hard. So, why don’t we just build the frontend like the backend?

Building it like a backend application

A good old web application usually consists of a router mapping the current url to a controller action which will render the whole page including the outer layout. The big picture looks like this:

By replicating this cycle in our frontend application we can - together with react - reach an easier to understand application design with less mutable parts.

Implementing it

When the user is navigating to a different view, we will just use links. When the user clicks a link an event handler will tell the router to render the new page (by using window.history.pushState). Our frontend application flow now looks like this:

This leads to a one-directional data flow which is easier to reason about than a bidirectional data flow which you can usually find in frontend applications.

Let’s take a look to some example code.

The Application Component

// Application.jsx
import Router from "./Router"
import { routes as postRoutes } from "./PostView"
import { routes as accountRoutes } from "./AccountView"

class Application {
    render() {
        if (this.state.user) {
            return <Router routes={this._routes()} url={this.state.url}/>
        } else {
            // not logged in
            return <Router routes={this._routesWhenNotLoggedIn()} url={this.state.url}/>
        }
    }

    _routes() {
        // merge the routes of all the modules
        return [].concat(postRoutes(this.state.user), accountRoutes(this.state.user))
    }

    componentDidMount() {
        let oldPushState = window.history.pushState

        window.history.pushState = (state, title, url) => {
            this.setState({url: url, user: state})

            oldPushState.call(window.history, state, title, url)
        };
        window.onpopstate = (event) => {
            this.setState({url: window.location.pathname})
        };

        // after initial page load use the current url to kick off the app
        this.setState({url: window.location.pathname})
    }
}

// Router.jsx
export default class Router {
    render() {
        // find matching regular expression
        let routeIndex = routes.findIndex(route => {
            let regularExpression = route[0];
            return this.props.url.match(regularExpression) !== null;
        });

        if (routeIndex === -1) {
            throw new Error(`${this.props.url} not found`)
        } else {
            let matches = this.props.url.matches(routes[routeIndex][0])
            // call route handler with regex matches
            return routes[routeIndex][1].apply(this, matches.slice(1))
        }
    }
}

We have an Application component which will intercept window.history.pushState calls to rerender the page when the url changes. The Application also keeps a user as it’s state. The user is just a plain old javascript object[0]. If the user has some relations (e.g. the user has some blog posts) these relations will be part of the user object, so that you only have one single application data state. So the major mutable parts of the whole application are capsulated in the Application.

Once the user has logged in Application.state.user will be set by calling window.history.pushState(user, undefined, undefined) from the login view. Remember: The first argument of pushState can be any javascript object.

If a view is updating the user (e.g. account settings) it will not mutate the user object, instead it will create a copy and update the copy. Then it will use window.history.pushState(newUser, undefined, undefined) to push the updated user object to the Application[1]. The views always have the latest user object by design, the views cannot get out of sync with the data. Also there is no mutable data, awesome!

The user object is passed down from the Application to the views by currying the route actions (postAction in the code below) with the user.

// Link.jsx
class Link {
    render() {
        <a href={this.props.to} onClick={this._handleClick}>{this.props.children}</a>
    }

    _handleClick(event) {
        event.preventDefault()
        window.history.pushState({}, null, this.props.to)
    }
}

A Link component will render good old <a href="..">..</a> elements, but will intercept the onClick event to use window.history.pushState instead of doing a real http request.

The Views

// PostView.jsx
import { accountUrl } from "./AccountView"
class PostView extends {
    render() {
        return <OuterLayout>
            <h1>{this.props.title}</h1>
            <section>{this.props.body}</section>
            <Link to={accountUrl(this.props.user)}>View my account</Link>
        </OuterLayout>
    }
}

function postAction(user) {
    return postId => <PostView user={user} id={postId}/>
}

export const routes = user => [
    [new Regex('^/posts/(\d+)$'), postAction(user)]
]

// AccountView.jsx
export const accountUrl = user =>
    `/my-account`

You can see that the view modules only export their routes and their url generator functions. So communication across views only happens via Links or more specifically by routing.

It’s the views responsibility to display the full page including an outer layout. This is just like a view in rails or symfony. The best way to do this is by introducing an OuterLayout component.

Why not flux

Flux is IMO a great pattern for building hybrid react apps (so not using react everywhere). In case you’re using a pure react based application you don’t need data stores and dispatchers. Data stores and dispatchers lead to mutable state and indirect code. Having multiple data stores also introduces multiple sources of truth, which increases complexity. If your app is only using react for some parts flux is a great solution, but if you are using react for everything you are better of with direct code and immutable data.

Takeaways

By building your react application like a backend application - introducing clearly seperated views, communicating only via urls - you will archieve a simple single directed data flow. By using immutable data structures the only major state (so, ignoring ui state like animation state) is a tuple of the user data structure and the current url.

Thanks for reading :) If you have any suggestions let me know!

Check out my personal blog if you’re interested in more of this stuff!

[0] An example user object could look like this:

{ id: 1, email: '[email protected]', posts: [{title: 'My first post', content: 'Lorem ipsum'}] }.

[1] Usually you do this after sending an ajax request to the server. With promises this could look like this:

// The user has changed his email via the AccountView
updateEmail(newEmail)
    .then(() => {
        let newUser = Object.assign({}, this.props.user, {email: newEmail})
        window.history.pushState(newUser, undefined, undefined)
    })
    .catch(error => ...)

where updateEmail is doing an ajax request and returning a Promise.