February 2016

Volume 31 Number 2

[ASP.NET]

Progressive Enhancement with ASP.NET and React

By Graham Mendick

Delivering Web content reliably is a game of chance. The random elements are the user’s network speed and browser capabilities. Progressive enhancement, or PE, is a development technique that embraces this unpredictability. The cornerstone of PE is server-side rendering of the HTML. It’s the only way to maximize your chances of success in the game of content delivery. For users with a modern browser, you layer on JavaScript to enhance the experience.

With the advent of data-binding libraries like AngularJS and Knockout, the Single-Page Application (SPA) came into its own. The SPA is the antithesis of PE because, by client rendering the HTML, it ignores the unpredictable nature of the Web. Visitors on slow networks face long loading times while users and search engines with less-capable browsers might not receive any content at all. But even these concerns haven’t blunted the appeal of the SPA.

The SPA killed progressive enhancement. It was just too hard to turn a server-rendered application into a SPA through JavaScript enhancement.

Luckily, it turns out that PE isn’t really dead. It’s merely been sleeping, and a JavaScript library called React has just come along and woken it up. React provides the best of both worlds because it can run on the server and on the client. You can start with a server-­rendered application and, at the flick of a switch, bring it to life as a client-rendered one.

The TodoMVC project (todomvc.com) offers the same Todo SPA built with different JavaScript data-binding libraries to help you decide which to choose. It’s a great project, but the implementations suffer from being client-rendered only. In this article, I’ll put this right by building a cut-down version as a progressively enhanced SPA using React and ASP.NET. I’ll concentrate on the read-only functionality, so you’ll be able to view your list of todos and filter them to show the active or completed ones.

Rendering on the Server

With the old approach to PE, I’d build an ASP.NET MVC application using Razor to render the todo list on the server. If I decided to enhance it into a SPA, I’d be back at square one—I’d have to re-implement the rendering logic in JavaScript. With my new approach to PE, I’ll build an ASP.NET MVC application using React instead of Razor to render the todo list on the server. This way, it can double as the client-rendering code.

I’ll start by creating a new ASP.NET MVC project called Todo­MVC. Aside from the View layer, the code is unremarkable, so the Models folder holds a TodoRepository that returns an IEnumerable of todos, and inside the HomeController is an Index method that calls into the repository. From that point on, things start to look a bit different. Instead of passing the todo list to the Razor view, I’ll pass it to React to produce the HTML on the server.

To run JavaScript on the server you need Node.js, which you can download from nodejs.org. Node.js comes with its own package manager called npm. I’ll install React using npm just as I’d use NuGet to install a .NET package. I’ll open a command prompt, cd into my TodoMVC project folder and run the “npm install react” command.

Next, I’ll create a file called app.jsx in the Scripts folder (I’ll explain the .jsx file extension shortly). This file will hold the React rendering logic, taking the place of the Razor view in a typical ASP.NET MVC project. Node.js uses a module-loading system so, to load the React module, I’ll add a require statement at the start of app.jsx:

var React = require('react');

A React UI is made up of components. Each component has a render function that turns input data into HTML. The input data is passed in as properties. Inside app.jsx, I’ll create a List component that takes in the todos and outputs them as an unordered list, with the title of each todo represented as a list item:

var List = React.createClass({
  render: function () {
    var todos = this.props.todos.map(function (todo) {
      return <li key={todo.Id}>{todo.Title}</li>;
    });
    return <ul>{todos}</ul>;
  }
});

The file has a .jsx extension because the React code is a mixture of JavaScript and an HTML-like syntax called JSX. I want to run this code on the server, but Node.js doesn’t understand JSX so I must first convert the file into JavaScript. Converting JSX to JavaScript is known as transpiling, and an example transpiler is Babel. I could paste my app.jsx contents into the online Babel transpiler (babeljs.io/repl) and create an app.js file from the transpiled output. But it makes more sense to automate this step because app.jsx could change fairly often.

I’ll use Gulp to automate the conversion of app.jsx into app.js. Gulp is a JavaScript task runner that comes with a variety of plug-ins to help you transform source files. Later on, I’ll write a Gulp task that bundles up the JavaScript for the browser. For now, I need a task that passes app.jsx through the Babel transpiler so it can be used inside Node.js on the server. I’ll install Gulp and the Babel plug-in from npm by running:

npm install gulp gulp-babel babel-preset-react

As you can see, by separating the package names with spaces, I can install multiple packages with a single command. I’ll create a gulpfile.js inside the TodoMVC project folder and add the transpile task to it:

var babel = require('gulp-babel');
gulp.task('transpile', function(){
  return gulp.src('Scripts/app.jsx')
    .pipe(babel({ presets: ['react'] }))
    .pipe(gulp.dest('Scripts/'))
});

The task is made up of three steps. First, Gulp receives the app.jsx source file. Then, the file is piped through the Babel transpiler. Last, the output app.js file is saved to the Scripts folder. To make the task runnable, I’ll use Notepad to create a package.json file in the TodoMVC project folder with a scripts entry pointing at it:

{
  "scripts": {
    "transpile": "gulp transpile"
  }
}

From the command line I’ll run the transpile task using “npm run transpile.” This generates an app.js file that can run inside Node.js because the JSX has been replaced with JavaScript.

Because I’m using React as the view layer, I want to pass the todos from the controller into the List component and have the HTML returned. In Node.js, the code inside app.js is private and can only be made public by explicitly exporting it. I’ll export a getList function from app.jsx so the List component can be created externally, remembering to run the transpile task so that app.js is updated:

function getList(todos) {
  return <List todos={todos} />;
}
exports.getList = getList;

The HomeController is in C# and the getList function is in JavaScript. To call across this boundary, I’ll use Edge.js (tjanczuk.github.io/edge), which is available from NuGet by running Install-Package Edge.js. Edge.js expects you to pass it a C# string containing Node.js code that returns a function with two parameters. The first parameter holds the data passed from the C# and the second parameter is a callback used to return the JavaScript data back to the C#. After running “npm install react-dom” to bring in React’s server-rendering capability, I’ll use Edge.js to create a function that returns the List component’s HTML from the todos array passed in:

private static Func<object, Task<object>> render = Edge.Func(@"
  var app = require('../../Scripts/app.js');
  var ReactDOMServer = require('react-dom/server');
  return function (todos, callback) {
    var list = app.getList(todos);
    callback(null, ReactDOMServer.renderToString(list));
  }
");

From the Node.js code, Edge.js creates a C# Func, which I assign to a variable called “render” in the HomeController. Calling render with a list of todos will return the HTML. I’ll add this call into the Index method, using the async/await pattern because calls into Edge.js are asynchronous:

public async Task<ActionResult> Index()
{
  var todos = new TodoRepository().Todos.ToList();
  ViewBag.List = (string) await render(todos);
  return View();
}

I added the HTML returned to the dynamic ViewBag so I can access it from the Razor view. Even though React is doing all the work, I still need one line of Razor to send the HTML to the browser and complete the server rendering:

<div id="content">@Html.Raw(ViewBag.List)</div>

This new approach to progressive enhancement might seem like more work compared to the old approach. But don’t forget, with this new approach, the server-rendering code will become the client-rendering code. There won’t be the duplicated effort required by the old approach when it comes to turning the server-rendered application into a SPA.

Filtering on the Server

The todos must be filterable so that either the active or completed ones can be displayed. Filtering means hyperlinks and hyperlinks mean routing. I’ve just substituted Razor for React, a JavaScript renderer that works on both client and server. Next, I’m going to apply the same treatment to routing. Rather than use the routing solution that ships with ASP.NET MVC, I’m going to replace it with the Navigation router (grahammendick.github.io/navigation), a JavaScript router that works on both client and server.

I’ll run “npm install navigation” to bring in the router. You can think of the Navigation router as a state machine, where each state represents a different view within your application. In app.jsx, I’ll configure the router with a state representing the todo “list” view. I’ll assign that state a route with an optional “filter” parameter so that the filtering URLs look like “/active” and “/completed”:

var Navigation = require('navigation');
var config = [
  { key: 'todoMVC', initial: 'list', states: [
    { key: 'list', route: '{filter?}' }]
  }
];
Navigation.StateInfoConfig.build(config);

With the old approach to PE, you’d put the filtering logic inside the controller. With the new approach, the filtering logic lives inside the React code so it can be reused on the client when I turn it into a SPA. The List component will take in the filter and check it against a todo’s completed status to determine the list items to display:

var filter = this.props.filter;
var todoFilter = function(todo){
  return !filter || (filter === 'active' && !todo.Completed)
    || (filter === 'completed' && todo.Completed);
}
var todos = this.props.todos.filter(todoFilter).map(function(todo) {
  return <li key={todo.Id}>{todo.Title}</li>;
});

I’ll change the HTML returned from the List component to include the filter hyperlinks below the filtered todo list:

<div>
  <ul>{todos}</ul>
  <ul>
    <li><a href="/">All</a></li>
    <li><a href="/active">Active</a></li>
    <li><a href="/completed">Completed</a></li>
  </ul>
</div>

The exported “getList” function needs an additional parameter so it can pass the new filter property into the List component. This is the last change to app.jsx to support filtering, so it’s a good time to rerun the Gulp transpile task to generate a fresh app.js.

function getList(todos, filter) {
  return <List todos={todos} filter={filter} />;
}

The selected filter must be extracted from the URL. You might be tempted to register an ASP.NET MVC route so that the filter is passed into the controller. But this would duplicate the route already configured in the Navigation router. Instead, I’ll use the Navigation router to extract the filter parameter. First, I’ll remove all mention of route parameters from the C# RouteConfig class.

routes.MapRoute(
  name: "Default",
   url: "{*url}",
  defaults: new { controller = "Home", action = "Index" }
);

The Navigation router has a navigateLink function for parsing URLs. You hand it a URL and it stores the extracted data in a StateContext object. You can then access this data using the name of the route parameter as the key:

Navigation.StateController.navigateLink('/completed');
var filter = Navigation.StateContext.data.filter;

I’ll plug this route parameter extraction code into the render Edge.js function so the filter can be retrieved from the current URL and passed into the getList function. But the JavaScript on the server can’t access the URL of the current request, so it’ll have to be passed in from the C#, along with the todos, via the function’s first parameter:

return function (data, callback) {
  Navigation.StateController.navigateLink(data.Url);
  var filter = Navigation.StateContext.data.filter;
  var list = app.getList(data.Todos, filter);
  callback(null, ReactDOMServer.renderToString(list));
}

The corresponding change to the Index method of the HomeController is to pass an object into the render call that holds both the URL from the server-side request and the todo list.

var data = new {
  Url = Request.Url.PathAndQuery,
  Todos = todos
};
ViewBag.List = (string) await render(data);

With the filtering in place, the server-side phase of the build is complete. Starting with server-rendering guarantees, the todo list is viewable by all browsers and search engines. The plan is to enhance the experience for modern browsers by filtering the todo list on the client. The Navigation router will manage the browser history and ensure that a client-filtered todo list remains bookmarkable.

Rendering on the Client

If I’d built the UI with Razor, I’d be no closer now to the SPA finishing line than when I set out. Having to replicate the rendering logic in JavaScript is why old school PE fell out of favor. But, with React, it’s quite the opposite because I can reuse all my app.js code on the client. Just as I used React to render the List component to HTML on the server, I’ll use it to render that same component to the DOM on the client.

To render the List component on the client I need access to the todos. I’ll make the todos available by sending them down in a JavaScript variable as part of the server render. By adding the todo list to the ViewBag in the HomeController, I can serialize them to a JavaScript array inside the Razor view:

<script>
  var todos = 
    @Html.Raw(new JavaScriptSerializer().Serialize(ViewBag.Todos));
</script>

I’ll create a client.js file inside the Scripts folder to hold the client rendering logic. This code will look the same as the Node.js code I passed into Edge.js to handle the server rendering, but tweaked to cater to the differences in environment. So, the URL is sourced from the browser’s location object, rather than the server-side request, and React renders the List component into the content div, rather than to an HTML string:

var app = require('./app.js');
var ReactDOM = require('react-dom');
var Navigation = require('navigation');
Navigation.StateController.navigateLink(location.pathname);
var filter = Navigation.StateContext.data.filter;
var list = app.getList(todos, filter);
ReactDOM.render(list, document.getElementById('content'));

I’ll add a line to app.jsx that tells the Navigation router I’m using HTML5 History rather than the hash history default. If I didn’t do this, the navigateLink function would think that the URL had changed and update the browser hash to match:

Navigation.settings.historyManager = 
  new Navigation.HTML5HistoryManager();

If I could add a client.js script reference directly to the Razor view, that would be the end of the changes needed for client rendering. Unfortunately, it’s not quite that simple, because the require statements inside client.js are part of the Node.js module-loading system and aren’t recognized by browsers. I’ll use a Gulp plug-in called browserify to create a task that bundles client.js and all its required modules into a single JavaScript file, which I can then add to the Razor view. I’ll run “npm install browserify vinyl-source-stream” to bring in the plug-in:

var browserify = require('browserify');
var source = require('vinyl-source-stream');
gulp.task('bundle', ['transpile'], function(){
  return browserify('Scripts/client.js')
    .bundle()
    .pipe(source('bundle.js'))
    .pipe(gulp.dest('Scripts/'))
});

I don’t want the bundle task to run unless it includes the latest changes to app.jsx. To ensure that the transpile task always runs first, I made it a dependency of the bundle task. You can see that the second parameter of a Gulp task lists its dependencies. I’ll add an entry to the scripts section of package.json for the bundle task. Running the command “npm run bundle” will create bundle.js and I’ll add a script reference pointing at it to the bottom of the Razor view:

<script src="~/Scripts/bundle.js"></script>

By server-rendering the HTML, I’ve built an application that starts faster than those at todomvc.com because they can’t display any content until their JavaScript loads and executes. Similarly, once the JavaScript loads in my application, a client render runs. In contrast, this doesn’t update the DOM at all, but allows React to attach to the server-rendered content so that subsequent todo list filtering can be handled on the client.

Filtering on the Client

If you were doing PE the old-fashioned way, you might implement filtering on the client by toggling class names to control the todo items’ visibility. But without a JavaScript router to help out, it’s all too easy to break browser history. If you neglect to update the URL, the filtered list won’t be bookmarkable. By doing PE the modern way, I already have the Navigation router up and running on the client to keep the browser history intact.

To update the URL when a filter hyperlink is clicked, I need to intercept the click event and pass the hyperlink’s href into the router’s navigateLink function. There’s a React plug-in for the Navigation router that will handle this for me, provided I build the hyperlinks in the prescribed way. For example, instead of writing <a href="/active">Active</a>, I must use the RefreshLink React component the plug-in provides:

var RefreshLink = require('navigation-react').RefreshLink;
<RefreshLink toData={{filter: 'active'}}>Active</RefreshLink>

After running “npm install navigation-react” to bring in the plug-in, I’ll update the List component in app.jsx by replacing the three filter hyperlinks with their RefreshLink equivalents.

To keep the UI and URL in sync, I have to filter the todo list whenever the URL changes—not only when a filter hyperlink is clicked but also when the browser back button is pressed. Instead of adding separate event listeners, I can add a single listener to the Navigation router that will be called any time navigation happens. This navigation listener must be attached to the “list” state I created as part of the router configuration. First, I’ll access this state from the Navigation router using the keys from the configuration:

var todoMVC = Navigation.StateInfoConfig.dialogs.todoMVC;
var listState = todoMVC.states.list;

A navigation listener is a function assigned to the state’s “navigated” property. Whenever the URL changes, the Navigation router will call this function and pass in the data extracted from the URL. I’ll replace the code in client.js with a navigation listener that re-renders the List component into the “content” div using the new filter. React will take care of the rest, updating the DOM to display the freshly filtered todos:

listState.navigated = function(data){
  var list = app.getList(todos, data.filter);
  ReactDOM.render(list, document.getElementById('content'));
}

In implementing the filtering, I accidentally removed the code from client.js that triggered the initial client render. I’ll restore this functionality by adding a call to “Navigation.start” at the bottom of client.js. This effectively passes the current browser URL into the router’s navigateLink function, which triggers the navigation listener and performs the client rendering. I’ll rerun the bundle task to bring the latest changes into app.js and bundle.js.

The new approach to PE is modern day alchemy. It turns the base metal of a server-rendered application into SPA gold. But it takes a special kind of base metal for the transformation to work, one that’s built from JavaScript libraries that run equally well on the server and in the browser: React and the Navigation router in the place of Razor and ASP.NET MVC routing. This is the new chemistry for the Web.

Cutting the Mustard

The aim of PE is an application that works in all browsers, while offering an improved experience for modern browsers. But, in building this improved experience, I’ve stopped the todo list from working in older browsers. The SPA conversion relies on the HTML5 History API, which Internet Explorer 9, for example, doesn’t support.

PE isn’t about offering the same experience to all browsers. The todo list doesn’t have to be a SPA in Internet Explorer 9. In browsers that don’t support HTML5 History, I can fall back to the server-rendered application. I’ll change the Razor view to dynamically load bundle.js, so it’s only sent to browsers that support HTML5 History:

if (window.history && window.history.pushState) {
  var script = document.createElement('script');
  script.src = "/Scripts/bundle.js";
  document.body.appendChild(script);
}

This check is called “cutting the mustard” because only those browsers that meet the requirements are considered worthy of receiving the JavaScript. The end result is the Web equivalent of the optical illusion where the same picture can either look like a rabbit or a duck. Take a look at the todo list through a modern browser and it’s a SPA, but squint at it using an old browser and it’s a traditional client-server application.


Graham Mendick believes in a Web accessible to all and is excited by the new possibilities for progressive enhancement that isomorphic JavaScript has opened up. He’s the author of the Navigation JavaScript router, which he hopes will help people to go isomorphic. Get in touch with him on Twitter: @grahammendick.

Thanks to the following Microsoft technical expert for reviewing this article: Steve Sanderson
Steve Sanderson is a Web developer on the ASP.NET team at Microsoft. His current focus is on making ASP.NET great for developers building rich JavaScript applications.