Single page application in React on ASP.NET Core
Single page application in React on ASP.NET Core
What is React
React is an open-source client-side JavaScript framework for building user interfaces. It is developed by Facebook and used in their own products. It is also used by some other mayor web-sites like Netflix, imgur, Weather Underground and Feedly.
The two main reasons why they developed React were performance and simplicity. To achieve this React moves away from templates and data-binding but uses JavaScript components and a one-way data flow instead. This combined with a Virtual DOM decreases the number of updates to the real DOM and makes the user interfaces more responsive to data changes and user interactions. It this post I describe briefly how to develop an single page application in React on ASP.NET Core.
Creating React components
The React JavaScript Components represent a part of the view that will be rendered. They use JSX to describe the HTML tags needed to render the components on the page. JSX is a JavaScript extension that allows developers to define XML-like tags in their JavaScript code. This extension is not supported by browsers out-of-the-box but a transpiler like Babel is used to covert JSX into standard JavaScript. More about this later in this post.
1 2 3 4 5 6 7 8 9 10 11 12 13 | class MainPage extends Component { render() { return ( <div> <Header userAuthenticated={this.props.loggedOn} userName={this.props.name} /> {this.props.children} <div className='container'> <Footer /> </div> </div> ); } } |
In this example renders the MainPage component a div tag with some child components. You also see that it passes data to the header by assigning values to the attributes userAuthenticated and userName.
Declaring a style for components is as easy as assigning the style as Json object to the style property of a component in the render method
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | var separatorStyle = { borderBottom: '1px solid #ccc', fontSize: '1px', height: '8px', marginBottom: '8px' }; class Footer extends Component { render() { return ( <div> <div style={separatorStyle}/> <div>© 2015-2016 Soloco</div> </div> ); } } |
Similar like we route url’s on server side to specific views we use a router on the client side to map specific urls to a view.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class ApplicationRouter extends Component { render() { return ( <ReduxRouter> <Route path="/" component={MainPage}> <IndexRoute component={LogonView}/>. <Route path="home" component={HomeView} /> <Route path="about" component={AboutView} /> <Route path="register" component={RegisterView} /> <Route path="vehicles" component={VehiclesView} /> <Route path="logon" component={LogonView} /> <Route path="documentation" component={DocumentationView} /> <Route path="documentation/:id" component={DocumentationView}/> </Route> <Route path="*" component={NotFoundPage}/> </ReduxRouter> ); } } export default ApplicationRouter; |
One-way data flow
Components are structured in an hierarchical way and they use two types of data to render the HTML. They received input data (accessed via this.props) from the parent component. And a component can also maintain internal state data (accessed via this.state). The differences are outlined in detail here. When the data of a component changes, the component is rendered again based on the updated data. Properties also support property validation to ensure the data is correct during development.
Managing state with Flux and Reflux
To achieve high performance React recommends using a single immutable state with unidirectional data-flow to render your application. This is supported by the Flux architecture and the Redux library.
- API this is the back-end API used for authorization, documentation and real-time data of the vehicles written in ASP.NET Core. This API is accessible over HTTP and web-sockets.
- Service encapsulates the logic that calls the API or receives push messages from the API over web-sockets. This is a bit different as in most Redux examples but it seems more logic than placing the API calls in the Views or in the Action’s itself. This makes the Actions and Reducers only responsible for state changes. This also makes the the React Components lighter and easier testable because they don’t need to know about the action creators and the dispatcher.
- The Store holds the single immutable state. This state is composed out of multiple reducers each responsible for a part of the state.1234{user: { ... // this is the user state managed by the users reducer },vehicles: { ... // this is the vehicles state managed by the vehicles reducer },}
When the state is constructed or updated each reducer is called to get a part to the state. And these parts are combined into the single immutable application state. - Actions are methods that create action objects to update the state. Action objects are simple JavaScript objects that represents what is is or should change in the state. These objects are dispatched to the store in order to update the state.1234567documentLoaded: function(id, document) {return dispatch({type: actionsDefinitions.DOCUMENT_LOADED,id: id,document: document});},
- Reducers handle the actions and return the a new version of the state changed according to the received action.12345678910111213141516171819export function reducer(state = {}, action) {switch (action.type) {case actionsDefinitions.LOADED:return {headers: action.documents};case actionsDefinitions.ERROR:return {headers: action.error};case actionsDefinitions.DOCUMENT_LOADED:var documents = state.documents ? { ...state.documents } : {};documents[action.id] = action.document;return {headers: state.headers,documents: documents};
- Component are are the main views of the application. These views are connected to the global store by the connect method. This ensure that the view is notified when the state has changed. When the state is changed the mapStateToProps() method is invoked. This method transforms the state into the data (props) necessary to render the component.123456789101112import React, { PropTypes, Component } from 'react';import { connect } from 'react-redux';import { Button, Panel, Jumbotron } from 'react-bootstrap';import { actions as userActions, userStatus } from '../../state/user'import Header from './Header';import Footer from './Footer';class MainPage extends Component {render() {return (
{this.props.children}
); } } MainPage.propTypes = { loggedOn: PropTypes.bool, name: PropTypes.string }; function mapStateToProps(state) { return { loggedOn: state.user.status === userStatus.authenticated, name: state.user.name }; } export default connect(mapStateToProps)(MainPage);
- Sub-component are declared by component. They are not connected to the store directly but receive their data from the parent document by using the properies. (this.props)
Please have a look at the official Flux and Redux documentation for more detailed infromation about this pattern. In the sample application I diverge a bit from the I encapsulated
RealTimeWeb currently doesn’t support server-side rendering but it will be added in the near future…
Using ES2015
The JavaScript language has improved quit a lot last years and the latest standard includes some really powerful features to create more readable and maintainable code.
ECMAScript 6 is the newest version of the ECMAScript standard. This standard was ratified in June 2015. ES2015 is a significant update to the language, and the first major update to the language since ES5 was standardized in 2009. Implementation of these features in major JavaScript engines is underway now.
Quote taken from the Babel ES2015 page. ES2015 is however not yet supported by most browsers, but that doesn’t prevent us from using the features already. The langue can be used by processing the JavaScript files by the Babel transpiler. This tool convert the ES2015 source code into plain JavaScript understandable by the browsers. We also use this to tranform JSX code into JavaScript.
Here are the 4 most powerful JavaScript features used in the sample application:
- Modules are now supported at language level by using the import and export statements.1234567import { Router, Route, Link, IndexRoute } from 'react-router';import MainPage from './components/MainPage';export default {initialize: function() { ... }}
- By adding arrows functions () => {}, also called lambda functions, JavaScript finally supports the condensed way to declare anonymous functions as supported in most recent language.12this.props.errors.map(message => errors.push(({message}
) ));
- Classes and inheritance are now supported by a class and extends statements. Note that this is still prototype-based inheritance and that it is a different model in comparison to other class-based programming languages.1class LogonPage extends Component {
- Spread and rest operators (…). Spread and rest operators are used to split and combine arrays or objects. Especially the object spread operator is really powerful because it can be used to clone and extend objects.1234567891011//Create a copy of stateconst vehicles = { ...state };//clone and extend vehicle with some properties//(property values are overridden if they exist)const extendedVehicle = {...vehicle,id: action.id,name: action.name,state: 'Driving from ' + action.origin + ' to ' + action.destination,};
For a coprehensive list of the new features take a look at the Babel web-site
Managing JavaScript dependencies
Modern web-application consist of a large part of JavaScript code these days. And whole range of JavaScript libraries are available to use. These libraries are managed by the node package manager (npm). Npm uses a package.json file in the root of the web-project to keep track of the dependencies. This makes it easy to (re)install the necessary JavaScript dependencies by calling npm install
from the command line or during the build process.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | { "name": "Soloco.RealTimeWeb", "private": true, "engines": { "node": ">= 0.12", "iojs": ">= 3.0", "npm": ">= 2.14.2" }, "dependencies": { "react": "0.14.6", "react-bootstrap": "0.28.2", "react-dom": "^0.14.3", "react-redux": "4.0.6", "react-router": "1.0.3", "react-tap-event-plugin": "0.2.1", "react-google-maps": "^4.7.2", "redux": "3.0.5", "redux-devtools": "^3.0.1", ... } |
The main libraries used in RealTimeWeb are:
- React, the client side library developed by Facebook, for creating a single page application
- Redux for managing state in the javascript application
- React bootstrap enabling the usage of the Twitter Bootstrap library from React
Gulp scripts
Gulp is a node.js task runner that supports many plug-ins. It is used to create build tasks and watchers. The most important plug-ins used in the application are Browserify and Babelify. Browserify combines all JavaScripts files into a single downloadable file. And Babelify transpiles ES6/7 and JSX into plain JavaScript to enable the usage of new JS features and syntax.
The main build task preforms following task in sequence:
- Clean the wwwroot folder. This is the folders that is used by the web-server to respond requests of the static files of the web-application
- Browserify is used to combine Client/src/app.js and all its dependencies into a single JavaScript file wwwroot/scripts/app.js. Babelify is used to support ES6/7 and JSX features. Some libraries like React and Redux and are excluded from this file and are combined in a different vendor.js file.
- Browserify is used to combine libraries like React and Redux in a wwwroot/scripts/vendor.js file. This improve the build time of the app.js file drastically. It also improves the load time of the page because these much less frequently changed in comparison to the application files.
- Static files are copied from Client/statics to wwwroot
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | var config = { target: './wwwroot/', app: 'Client/src/app.js', appWatch: 'Client/src/**/*.*', appTarget: 'scripts/app.js', vendorTarget: 'scripts/vendor.js', vendorFiles: [ 'react', 'react-dom', 'react-router', 'react-bootstrap', 'react-redux', 'redux', 'redux-router', 'history', 'redux-thunk', 'reqwest', 'store' ], statics: [ './Client/statics/**/*.*' ], documentation: [ '../../doc/**/*.*' ], styles: { source: ['Client/src/**/*.less'], destination: 'app.css' }, tools: './Client/tools/**/*.js' }; gulp.task('application', function () { return browserify(config.app, { debug: true }) .transform("babelify", { presets: ["es2015-loose", "react", "stage-0"] }) .external(config.vendorFiles) .bundle() .on('error', function (err) { console.log('Error: ' + err.message); this.emit('end'); }) .pipe(source(config.appTarget)) .pipe(gulp.dest(config.target)); }); gulp.task('vendor', function () { return browserify(null, { debug: true }) .require(config.vendorFiles) .transform("babelify", { presets: ["es2015-loose", "react", "stage-0"] }) .bundle() .on('error', function (err) { console.log('Error: ' + err.message); this.emit('end'); }) .pipe(source(config.vendorTarget)) .pipe(gulp.dest(config.target)); }); |
Check out Client/tools/build.js for the full gulp script.
Command line tools
Using command-line tools can improve the productivity during development. Here I present you the tools used to build and test the JavaScript application. All tools should be executed from the web-application folder src/Soloco.RealTimeWeb
To build all front-end files files from client to wwwroot folder the gulp build script is executed:
1 | gulp build |
Watch for changes in JS files and build instantly. This is the tool that you will keep open in a console window during development.
1 | gulp build-watch |
Build and copy only application files. This performs a faster build because it doesn’t clean the wwwroot folder and doesn’t build the vendor.js. This can be uses during development
1 | gulp build-dev |
Run all JS tests:
1 | npm run test |
Run all JS tests and watch for changes. You can keep this running in a separated terminal during the development of the JavaScript application and tests.
1 | npm run test-watch |
Using React on ASP.NET Core
Loading the React SPA in a ASP.NET Core view is as easy as including the generated app.js and vendor.js files in the generated HTML file
1 | ... |
Testing the React application
While developing a React application I found these three kinds of tests really valuable.
- Testing of the actions and their effect on the state.1234567891011121314151617181920212223242526272829303132333435363738394041424344454647describe('State', () => {describe('User', () => {var store;beforeEach(function() {store = createStore(reducers);dispatcher.set(store.dispatch);});function assertState(expected) {var state = store.getState();expect(state.user).toEqual(expected);}it('should should be notAuthenticated by default', () => {assertState({status: userStatus.notAuthenticated});});it('should should be able to login succesful', () => {actions.loggedOn('tim', true);assertState({status: userStatus.authenticated,name: 'tim'});});it('should should be able to start login', () => {actions.logon();assertState({status: userStatus.notAuthenticated,logon: {}});});it('should should be able to login pending', () => {actions.logonPending();assertState({status: userStatus.notAuthenticated,logon: {pending: true}});});...
- Mapping of the state to component properties.12345678910111213141516171819202122232425262728293031describe('Props', () => {let store;beforeEach(function() {store = createStore(reducers);dispatcher.set(store.dispatch);});function assertProps(expected, id) {const params = id ? { routeParams: { id: id} } : null;const props = mapStateToProps(store.getState(), params);expect(props).toEqual(expected);}it('should have the default props', () => {assertProps({document: null,id: null,headers: []});});it('should have the header when documents are loaded', () => {actions.loaded([ 'h1', 'h2' ]);assertProps({document: null,id: null,headers: [ 'h1', 'h2' ]});});
- Rendering of the component. For this we mock the called service by using __Rewire__. Currently this test only verifies that the view is rendered without any errors. While this is already valuable on it’s own, this test can (or should) be completed with assertions of the rendered view.1234567891011121314151617181920212223describe('Render', () => {var store;beforeEach(function() {store = createStore(reducers);dispatcher.set(store.dispatch);//Mock the documentation import (we don't want to acces the real API)const documentationMock = { getDocuments: () => {}};DocumentationViewRewireAPI.__Rewire__("documentation", documentationMock);});function assertRender() {const props = mapStateToProps(store.getState());const view = TestUtils.renderIntoDocument(, props);const viewNode = ReactDOM.findDOMNode(view);}it('should build without problems', () => {assertRender();});});
Next
In the next post I describe how I implemented authentication via OAuth for the React application.
Source code
The source code of the application can be found on github:
https://github.com/tim-cools/RealTimeWeb.NET
Warning!
The application is still work in progress and new features will be added in the next following weeks…
Some of the technologies and frameworks used in this application are pre-release and most likely to change. Currently it is based on RC1 of .NET Core. I try to update the code as soon as possible whenever the final version is released. Ping me on Twitter if you have questions or issues.
RealTimeWeb.NET Blog Posts
This post is part of a blog post series about RealTimeWeb.NET application.
- RealTimeWeb.NET – A real time ASP.NET Core application
- Getting started with RealTimeWeb.NET
- RealTimeWeb.NET Front-end
- Creating an ASP.NET Core web application
- Single page application in React on ASP.NET Core
- React Authentication on ASP.NET Core with OAuth and Identity
- Real-time data pushed by web-sockets (SignalR) on ASP.NET Core
- Server-side rendering
- Real-time back-end
- Operations
- ...
I congratulate you for the excellent work, very clear thank you very much for the information !! regards