Progressive Enhancement Enhanced

Every developer who listens to their heart will agree that progressive enhancement is the right way to build web apps. It’s the only development approach that embraces the idea that web content should be accessible to all users, regardless of browser or network. So why isn’t Progressive Enhancement, or PE, more popular?

For PE to be a viable approach, it must produce an app that’s easy to enhance. Because, without the enhancements, the app will be sluggish for all user interactions other than the initial page load. But traditional PE, by starting with the server rendered version, produces an app that’s too hard to enhance. With no client side abstraction to fall back on, we’re forced into writing messy DOM manipulation code.

With React, there’s a new way to do PE. We start with a basic client rendered version and then use React’s isomorphic capabilities to render this same app on the server. The end result is an app that’s easy to enhance because we have a React Component hierarchy on the client to interact with instead of the DOM.

All the examples I’ve seen use Node for the web server. To redress the imbalance, I’ve built an example using ASP.NET. It’s a typical master/details scenario consisting of two views. The first displays a list of people and the second an individual person’s details. In keeping with the new approach to PE, let’s start with the client rendering.

Client Rendering

First come the React Components, one for each view. We’ll keep the rendering of our Components synchronous because it will simplify matters when we come to the server rendering. That means keeping the Components free of data access code. Here’s what our Details Component looks like, where the selected person is passed in fully populated:

var Details = React.createClass({
    render: function() {
        var person = this.props.person;
        return (
            <div>
                <h2>{person.Name}</h2>
                <div>Date of Birth</div>
                <div>{person.DateOfBirth}</div>
            </div>
        );
    }
});

With this new approach to PE, the role of the JavaScript router cannot be overstated. It will be the making or breaking of our app. For reasons that will become apparent as you read on, we’re using the Navigation router. Here’s the Navigation router configuration for the two routes in our example master/details app:

var stateNavigator = new Navigation.StateNavigator([
    {key: 'people', route: ''},
    {key: 'person', route: 'person/{id}'}
]);

The configuration is made up of States. There’s a ‘people’ State and a ‘person’ State, one for each view. Here’s how to build the Hyperlink that selects a person, where the stateKey refers to the key of the destination State and the navigationData holds the route data:

<NavigationLink 
    stateKey="person"
    navigationData={{id: person.Id}}
    stateNavigator={stateNavigator}>
    {person.Name}
</NavigationLink>

We want to display the Details Component when this Hyperlink is clicked. We’ll do this by attaching a navigated function to the ‘person’ State. The Navigation router will automatically call this function whenever the ‘person’ State is navigated to. Inside this function we’ll ask React to render the Details Component into the content placeholder in our HTML:

var states = stateNavigator.states;
states.person.navigated = function() {
    React.render(<Details />, 
        document.getElementById('content'));
}

For the render to work, we must pass the selected person’s details as props to the Component. We need an ASP.NET JSON Web Api method so we can retrieve these details over Ajax:

public class PersonController : ApiController
{
    public Person Get(int id)
    {
        return new PersonRepository()
                       .People.First(p => p.Id == id);
    }
}

By creating a JSON Web Api we’ve inadvertently created two sets of routes. One set is held on the client for the Navigation router and the other is held on the server for the Web Api. Let’s do away with the separate Web Api configuration by reusing our client routes on the server.

Route Reuse

Routes in the Web Api are used to map requests to Controllers. So, if we plan to get rid of them, we’re going to need to replace the default Controller lookup mechanism. Instead, our custom Controller selector will use the Navigation router to do the request to Controller lookup. The Navigation router lets us map Urls to States by passing the requested Url into the start function:

stateNavigator.start(url);
var state = stateNavigator.stateContext.state;

To simplify the mapping, we’ve twinned the names of the Controllers to the names of the States. For example, a Url of ‘person/2’ returns the ‘person’ State which maps to the PersonController.

To run the Navigation router on the server, our JavaScript must be written inside Node using npm. This doesn’t mean duplicating our JavaScript because we’ll use browserify and gulp to bundle it up for use on the client. We’ll call into Node from C# using Edge.js, a library that lets us script Node in process.

Edge.js accepts a string containing the Node code and the JavaScript function to execute. Data is received from C# via the function’s first parameter and passed back from Node via its second callback parameter. Here is the C# getContext Func that calls Node passing in a Url and returns the matching State key:

Func<object, Task<object>> getContext = Edge.Func(@"
    var Navigation = require('navigation');
    var App = require('App');
 
    var stateNavigator = App.createStateNavigator();
    return function (url, callback) {
        stateNavigator.start(url);
        callback(null, stateNavigator.stateContext.state.key);
    }
");

Calls to Edge.js are async. In the Web Api, Message Handlers are the place to make async calls. We’ll create a ContextHandler Message Handler and override its SendAsync method passing the requested Url into our getContext Func. We’ll store the key returned in the request’s Properties collection so it can be accessed downstream by our custom Controller selector:

protected async override Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, 
    CancellationToken token)
{
    var url = request.RequestUri.PathAndQuery;
    var key = (string) await getContext(url);
    request.Properties["key"] = key;
    return await base.SendAsync(request, token);
}

We’ll build our Controller lookup mechanism by creating a class that implements IHttpControllerSelector. It’ll use the State key from the request Properties to return the associated Controller Type from the SelectController method:

public HttpControllerDescriptor SelectController(
    HttpRequestMessage request)
{
    return new HttpControllerDescriptor
    {
        Configuration = request.GetConfiguration(),
        ControllerType = Type.GetType(
            "NavigationEdgeApi.Controllers."
            + request.Properties["key"]
            + "Controller", true, true)
    };
}

In WebApiConfig, we’ll register our Message Handler and replace the default Controller selector with our new one:

config.MessageHandlers.Add(new ContextHandler());
config.Services.Replace(typeof(IHttpControllerSelector), 
    new ControllerSelector());

With our Controller selector in place, we’ll return to the client. Clicking the Hyperlink to select a person with id 2 from the list navigates to a Url of ‘person/2’. An Ajax call to our JSON Api passing this Url will return that person’s details because the Urls on the client and server now match.

We’ll place this Ajax request inside a navigating function attached to the ‘person’ State. The Navigation router will automatically call this function before the ‘person’ State is navigated to, giving us a chance to suspend the navigation until the Ajax call returns:

states.person.navigating = function(data, url, navigate) {
    getJSON(url, function(resp) {
        navigate({ person: resp });
    });
}

Our navigated function attached to the ‘person’ State is passed the JSON data in its second parameter. We’ll hand on the selected person as props to the Details Component:

states.person.navigated = function(data, asyncData) {
    React.render(<Details person={asynctData.person} />, 
        document.getElementById('content'));
}

We’ve got our Client rendering working with just a single set of routes. We don’t have a separate set of routes for the JSON Api. Not only does this make the code simpler, but it smooths the path for the server rendering.

Server Rendering

Imagine that, rather than left clicking the person selection Hyperlink, we right clicked and opened it in a new tab. The Url requested would still be ‘person/2’ but we must return the person’s details as HTML instead of JSON. Rather than cluttering up our Controllers with conditional logic, we’ll use content negotiation to return different responses based on the content type requested.

To avoid duplicating code, we’ll reuse our Details Component on the server to render the HTML. To help locate the Details Component based on the incoming request, we’ll add it to the ‘person’ State of our Navigation router configuration:

{key: 'person', route: 'person/{id}', component: Details}

Then we’ll use the Navigation router to look up the Component for a given Url, much like we did for our Controller lookup mechanism:

stateNavigator.start(url);
var component = stateNavigator.stateContext.state.component;

We’ll use Edge.js again to create a render Func that returns the HTML for a given Url. To perform the rendering for ‘person/2’, this Func needs the person JSON data so it can build the props to pass to the Details Component. Once the Component’s built, it can return the HTML via a call to React’s renderToString function:

Func<object, Task<object>> render = Edge.Func(@"
    var React = require('react');
    var Navigation = require('navigation');
    var App = require('App');
                 
    var stateNavigator = App.createStateNavigator();
    return function (data, callback) {
        stateNavigator.start(data.url);
        var props = {};
        props[stateNavigator.stateContext.state.key] = data.item;
        var component = React.createElement(
            stateNavigator.stateContext.state.component, props);
        callback(null, React.renderToString(component));
    }
");

We’ll create a RenderHandler Message Handler that intercepts the JSON response. If the request originated from an Ajax call then it lets the response continue unchanged. Otherwise, it passes the JSON into our render Func and overwrites the response with the HTML returned.

protected async override Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, 
    CancellationToken token)
{
    var response = await base.SendAsync(request, token);
    if (request.Content.Headers.ContentType == null)
    {
        var html = (string) await render(new { 
            url = request.RequestUri.PathAndQuery, 
            item = ((ObjectContent) response.Content).Value
        });
        response.Content = new StringContent(
            "<div id='content'>" + html + "</div>");
        response.Content.Headers.ContentType 
            = new MediaTypeHeaderValue("text/html");
    }
    return response;
}

The last piece of this PE puzzle is to get the server and client rendering working together. After the HTML is returned we want React to take over on the client so that subsequent Hyperlink clicks result in Ajax requests. By sending the props along with the server rendered HTML, we can trigger a client render after the page loads. This allows React to catch up with the server rendered content.

TL;DR

With isomorphic React, there’s a new way to do Progressive Enhancement that blows the old way out of the water. I’ve built an example using ASP.NET that shows this approach is open to all developers, not just those using Node for their web server. There’s not an ounce of duplicated code. Even the routes and JSON Api are shared across the client and server rendering.

Advertisements

2 thoughts on “Progressive Enhancement Enhanced

    1. Thanks. Aurelia doesn’t support server side rendering yet, does it? Also, Aurelia comes with its own router so I guess this technique wouldn’t work as well. But, I don’t know Aurelia that well, so please let me know if you think it would work.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s