Angular and React are both tools that are frequently used for building Single Page Applications (SPA). In order to do a comparison of them, I thought it would be useful to build an application using React that mirrored, as closely as possible, a well-known Angular application: the Hero app that features in Angular's documentation.
You can find a demo here.
Although Angular and React are often used for the same jobs, there are some significant differences between them. A particularly significant difference is that Angular is a framework whilst React is a library. What this means is that Angular aims to provide, out of the box, a comprehensive solution to every problem that you might have when developing an application. React, however, is only meant to do one thing, but to do that well - that is: rendering data into a template. Sometimes, React is called the V in MVC. If you need to do anything else (and for SPAs you probably will), then you will need other libraries to complement it. In fact, on this project I used several other libraries which I will name and link to as I go along.
In this article, I'll talk about my approach, how I solved certain problems, and what conclusions I've made regarding how React and Angular compare with one another.
The topics that I will discuss here are as follows: Routing, Dependency Injection (DI), Guards, Observables, Animation, and Lazy loading. Each will be covered under a separate section. The Angular Hero App can be found on Stack Blitz.
Routing
In an SPA, the actual webpage itself does not reload, but it can appear to the user as if it does: The content of the page and even the URL in the address bar can all change (sometimes dramatically). This trick is carried out via routing. In the simplest terms, the router is an object that listens to changes in the URL and runs code which is able to fetch data behind the scenes (via XHR) and re-render the content of the page.
As I hinted at before, React does not have routing out of the box, but there are several libraries that provide this functionality. For this app, I used React Router, which seems to be fairly highly regarded.
Configuring the Router
Routing in React using the React Router library is very different from routing in Angular. Whilst Angular routing involves configuring a large Javascript object, React Router routing is implemented using React Components. The implication of this is that React routes are dynamic. A React Route Component is mounted whenever its path matches the URL, but it also acts like a normal React Component and can be mounted according to other conditions. For example, a different Route can be shown on a mobile screen size than a desktop. This is not possible in Angular.
Here is an example of configuring a Route Component:
<Route path="/crisis-center" component={ CrisisListComponent }/>
You can also show as many components as you like for any one url, perhaps in different parts of the template. This is a little more tricky to accomplish in Angular where secondary routes provide something like the same functionality.
On the whole, React routing seems more flexible than Angular's. However, I did have difficulty getting the Route components to work with other components - in particular, with animation components. In the app, there is a warning appearing in the console when a redirect occurs within an animated component. This happens when you are logged out and click on the admin link. (Before I updated to a newer version of my animation library, this was an error rather than just a warning.)
Secondary Routes and Named Outlets
Secondary routes are Angular's way of having more than one route active at the same time. The term is a little misleading as secondary routes behave in exactly the same way as primary routes. What is different is how they are configured. Angular routes are rendered within router-outlet directives. For secondary routes, the router-outlet has a name attribute. For primary routes, the router-outlet is un-named. That's the difference.
Between Angular and React routes, the most significant difference is that Angular routes can change independently from each other. Different routes have their own representation within the URL itself. An example URL will help to explain this:
http://localhost:4200/crisis-center(popup:compose)
crisis-center is the primary route. compose is a secondary route that is displayed within the popup router-outlet. When the user navigates to /superheroes using the navigate() method, the (popup:compose) segment will persist and the secondary route will still be displayed:
http://localhost:4200/superheroes(popup:compose)
The Hero App uses a secondary route to display a pop up. Due to the reasons just discussed, this is difficult to implement using Route components in React.
The solution I came up with was not to use a Route at all, but just use a normal component that was shown and hidden using a toggle button.
Angular's secondary routes are an interesting feature. Conceptually they remind me of frames or portals. I would be interested to find out how much they are actually used since I can't think of any obvious use-cases.
Guards
A guard in Angular governs access to a route. It can both prevent navigation to a route and prevent navigation away from one.
React Router does not provide guards, so you have to create your own.
In the hero app, the admin page can only be reached if the user is logged in. A simple conditional sufficed to take care of this.
When the user is not logged in, they are redirected to the login page. For some reason, the redirect results in a warning in the console. This seems to be to do with the animation library I am using. I have still to get to the bottom of this.
if (this.props.adminservice.isloggedin()) {
return (
<div>
<h3>admin page
rest of page here...
</div>
)
} else {
return (<redirect to="/login?returnto=/admin"/>);
}
The redirect URL includes a query parameter that gives the address to return to. Once the user logs in, the app redirects them back from whence they came:
export default class Login extends Component {
...
redirectBack() {
const search = this.props.location.search.slice(1);
const params = queryString.parse(search);
const url = params.returnto || '/admin';
this.props.history.push(url);
}
logIn() {
this.props.adminService.logIn();
this.redirectBack();
}
...
}
Other guards in the Heroes app are guards for governing access to child routes, lazily loaded routes, and for preloading, or resolving, data. For this project, I did not implement these.
Observables
Although they are not an integral part of it, Angular uses Observables, (also known as streams), heavily. An Observable is a data source that periodically emits a value. I like to think of it as an array with an extra dimension of time. They have some similarities to promises and can often be used in place of them, such as for carrying out fetches for data from the server. One big advantage they have over promises is that they can be cancelled. This is super-useful when the app makes a call to the server and then the user navigates to another location whilst the request is in flight.
React can use Observables as well, of course, but, typically, React apps use Redux instead. Redux can also be made to work with Observables, but at the cost of much greater complexity - something that Redux is supposed to avoid.
Anyway, here's how I used Observables in my Heroes app:
@Inject('crisisService')
export default class CrisisDetailComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
id: -1,
name: '',
editName: ''
}
...
}
componentWillReceiveProps(nextProps) {
this.match.next(nextProps.match);
}
componentDidMount() {
this.match = new Rx.BehaviorSubject(this.props.match);
this.subscription = this.match
.map(match => match.params.id)
.switchMap(id => {
return Rx.Observable.fromPromise(this.props.crisisService.getCrisis(id));
})
.subscribe(crisis => {
this.setState(crisis);
});
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
...
render () {
...
}
}
In this case, the Observable `this.match` represents a stream of `match` objects. A match object represents a match between the route's path and the current location. It contains useful data about this map such as the portion of the URL that was matched.
The reason why an Observable makes sense in this case is because the same route can be matched by succeeding locations. A navigation isn't necessarily always to another page; It may be to the same page but with different parameters. In this case, the route component would not be unmounted, and so the same component can handle multiple matches.
For each match object, a new set of data is required to be fetched from the server. You can see how our `crisisService.getCrisis()` method returns a promise which we convert into an Observable.
When the request is in flight, the user could navigate away from the route prompting a new request. If we were not careful, an old request could come back and get mixed up with a newer one. It is thus important that whenever we make a new request the old request is cancelled. The `switchMap` method does this. In my opinion it is perhaps the 'killer feature' of Observables. What it does is map a stream of values to a stream of Observables and returns a new stream which emits the values emitted by these inner streams. The critical part of it is that when there is a new value on the input stream (our stream of match objects) all the inner streams for which we are still waiting for output on are cancelled.
We also want to make sure that the Observable is unsubscribed from when we unmount the component. You can see that we do this within the `componentWillUnmount` lifecycle method.
Dependency Injection
One of the most significant ways that Angular differentiates itself from React is in its use of *dependency injection*, or *DI* for short. DI is common in backend frameworks (the Spring framework in Java, for example) but less so in front end ones. DI is essentially a system for creating the runtime objects that make up an application. An application without DI will typically include a lot of code that instantiates objects and their dependencies. In a large system, this can quickly get very messy indeed. DI allows classes to simply declare (usually in some form of annotation) their dependencies, and DI will ensure that, when the app starts up, all the objects will be instantiated and have all the dependencies that they need.
I have written about DI elsewhere on this blog. In addition to helping to keep code much leaner and cleaner, it also decouples objects from their dependencies, which greatly assists in testing them in isolation.
Angular uses constructor injection to allow a class to declare what dependencies it needs. Here is an example:
constructor(
private route: ActivatedRoute,
private router: Router,
public dialogService: DialogService
) {}
Nothing else is required for the developer to write. When an instance of the class is created it will have those three properties on it.
Since React does not have a dependency injection system, I had to create it myself. It isn't possible to use constructor injection in React components because the constructor is called somewhere within React. Instead, what I do is use ES7 decorators for annotating dependencies on a component, and then the DI system adds the instantiated dependencies onto the `props` object.
@Inject('crisisService')
export default class CrisisDetailComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
id: -1,
name: '',
editName: ''
}
this.handleNameChange = this.handleNameChange.bind(this);
this.save = this.save.bind(this);
}
...
componentDidMount() {
this.match = new Rx.BehaviorSubject(this.props.match);
this.subscription = this.match
.map(match => match.params.id)
.switchMap(id => {
return Rx.Observable.fromPromise(this.props.crisisService.getCrisis(id));
})
.subscribe(crisis => {
this.setState(crisis);
});
}
}
Here is how services are configured with the container.
const config = [
{
key: 'heroService',
provider: HeroService
},{
key: 'crisisService',
provider: CrisisService
}, {
key: 'adminService',
provider: AdminService
}];
const render = Component => {
ReactDOM.render(
<AppContainer>
<Injector config={config}>
<Component />
</Injector>
</AppContainer>,
document.getElementById('app')
)
}
Note the `Injector` component. An Injector is the object at the heart of a DI system. It is on the Injector that we register all the providers for our system. The provider is simply a class that the system will instantiate when an object requests it. It is passed an array of configuration objects. Each object has both a *key* and a *provider*.
Angular's DI system is of course a lot more complicated, but this does the job. You can see the code for it here.
I do find the lack of DI in React a significant failing. I think React applications do suffer from tightly coupled objects as a result.
Animations
The Heroes app features very nice smooth transition effects between routes. I was very keen to be able to replicate these in my version.
Animations are created in React using the `react-transition-group` library. I did have to do a bit or work to get it to work with route transitions. In the end, I had to create a custom class which wraps the Route component.
export default class TransitionRoute extends React.Component {
render () {
const PageComponent = this.props.component;
return (
<Route
path={this.props.path}
children={( props ) =>(
<TransitionGroup component={firstChild}>
{
props.match &&
<CSSTransition
classNames="page"
timeout={400}
>
<PageComponent {...props}/>
</CSSTransition>
}
</TransitionGroup>
)}
/>
);
}
}
This class acts as a drop in for Route components, as you can see here:
<div className="container">
<TransitionRoute component={HeroListComponent} path="/superheroes" />
<TransitionRoute component={HeroDetailComponent} path="/hero/:id" />
<TransitionRoute component={CrisisCenterComponent} path="/crisis-center"/>
<TransitionRoute component={AdminComponent} path="/admin" />
<TransitionRoute component={LoginComponent} path="/login" />
</div>
Lazy Loading
Angular allows modules to be lazily loaded when the route that they are associated with are activated. This is set up in the route configuration via the `loadChildren` property.
{
path: 'crisis-center',
loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule',
data: { preload: true }
}
In order to do lazy load in React, I have used the react-loadable library. This is the library that is recommended on the React Router website. It does require a little boilerplate to be written. This is a higher order component which wraps the actual component.
import React, { Component } from 'react';
import Loadable from 'react-loadable';
class Loading extends Component {
render() {
return (<h1>loading.....</h1>);
}
}
const LoadableComponent = Loadable({
loader: () => import('./hero-list-component'),
loading: Loading,
});
export default class LoadableFoo extends React.Component {
render() {
return <LoadableComponent />;
}
}
The behavior is a little different from Angular. In Angular, the unit of lazy loading is a route. In React, any component can be lazy loaded when it becomes active. Obviously, this makes lazy loading a bit more fine grained in React.
Summary
My final judgement on using React to build SPAs is that it is perfectly adequate for the job providing that you are willing to do the necessary legwork to learn about other libraries. I hope that anyone reading this article and looking at my code will find this useful.
You can find the code here:
<a class="hero-link" href="https://github.com/Richardinho/react-hero-app-with-routing""> Github repository