Hello World: Comparing ReactJS And AngularJS
I love AngularJS. You know I love AngularJS. I write poems about AngularJS. But, at InVision App, some of the engineers have started using ReactJS; so, it's time that I start digging into ReactJS a bit such that I can remain relevant on the team and be able to jump in and fix bugs. Since I didn't know thing-one about ReactJS, I took some time over the weekend to go through React's "Getting Started" tutorial. And, after I was done with it, I thought it would be interesting to recreate the tutorial in AngularJS.
View this demo code in my JavaScript Demos project on GitHub.
Fundamentally, ReactJS and AngularJS are doing the same thing. Well, at least the relevant parts; React is a view-rendering framework whereas Angular is more of a JavaScript application platform. But, when you consider the parts of AngularJS that deal with the view-model and with the rendering of the DOM (Document Object Model), both React and Angular are solving the same problem. And, more or less, they're doing it in the same way.
The underlying mechanisms of each library are different. And, some philosophies are more thoroughly codified in the respective frameworks. But, when it comes down to it, each library is translating application state into a physical DOM tree that the browser renders.
NOTE: From what I have read, one of the benefits of dealing with a virtual DOM, as you do with ReactJS, is that you can render your views on the Server as well as on the Client. This is one of the main aspects of what people are calling "Isomorphic JavaScript."
On the ReactJS site, the getting started tutorial walks you through creating a Comment Box component. The following code is, more or less, the outcome of that tutorial, plus my personal coding style and general joie de vivre.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Hello React</title>
<script src="/vendor/reactjs/react-0.13.3.js"></script>
<script src="/vendor/reactjs/JSXTransformer-0.13.3.js"></script>
<script src="/vendor/jquery/jquery-2.1.0.min.js"></script>
<script src="/vendor/marked/marked-0.3.2.min.js"></script>
</head>
<body>
<h1>
Hello World: Comparing ReactJS to AngularJS
</h1>
<div id="content"></div>
<script type="text/jsx">
// I manage the comment box.
var CommentBox = React.createClass({
// I provide the initial view-model, before the component is mounted.
getInitialState: function() {
return({
data: []
});
},
// ---
// PUBLIC METHODS.
// ---
// I get called once after the component has initially been rendered. At
// this point, there is a physical DOM, including the window object, that
// can be consumed.
// --
// NOTE: The reason we are using this method to start polling for data is
// that we only want to poll on the client. Theoretically, we could be
// rendering this on the server through so-called "isomorphic JavaScript"
// where it wouldn't make any sense to poll for updates. This method
// indicates that we're actually working with a DOM (Document object model).
componentDidMount: function() {
// Load the initial data payload.
this.loadCommentsFromServer();
// Start polling the remote URL for new content.
setInterval( this.loadCommentsFromServer.bind( this ), this.props.pollInterval );
},
// I add a new comment to the collection.
handleCommentSubmit: function( comment ) {
var self = this;
// Optimistically add the new comment to the collection before
// we even hear back from the server.
// --
// NOTE: We are overwriting the data key of the current state, but
// leaving any other state keys in tact.
this.setState({
data: this.state.data.concat( comment )
});
$.ajax({
url: this.props.url,
dataType: "json",
type: "POST",
data: comment,
success: function handleSuccess( data ) {
// Store the new comments collection, overwriting the
// existing reference.
// --
// NOTE: We are overwriting the data key of the current state,
// but leaving any other state keys in tact.
self.setState({
data: data
});
},
error: function handleError( xhr, status, error ) {
console.error( "error", error );
}
});
},
// I render the view using the current state and properties collection.
render: function() {
return(
<div className="commentBox">
<h1>
Comments
</h1>
<CommentList comments={ this.state.data } />
<CommentForm onCommentSubmit={ this.handleCommentSubmit } />
</div>
);
},
// ---
// PRIVATE METHODS.
// ---
// I load the remote data from the server.
loadCommentsFromServer: function() {
var self = this;
$.ajax({
url: this.props.url,
dataType: "json",
cache: false,
success: function handleSuccess( data ) {
self.setState({
data: data
});
},
error: function handleError( xhr, status, error ) {
console.error( "Error", error );
}
});
}
});
// I manage the list of comments.
var CommentList = React.createClass({
// I render the view using the current state and properties collection.
render: function() {
// Translate each comment item into a comment component.
var commentNodes = this.props.comments.map(
function operator( comment, i, comments ) {
return(
<Comment author={ comment.author }>
{ comment.text }
</Comment>
);
}
);
return(
<div className="commentList">
{ commentNodes }
</div>
);
}
});
// I manage the individual comment.
var Comment = React.createClass({
// I render the view using the current state and properties collection.
render: function() {
// NOTE: Since processing markdown is an "expensive" action, I want to
// get a sense of how often this will run.
console.log( "Running marked on comment." );
var rawMarkup = marked(
this.props.children.toString(),
{
sanitize: true
}
);
return(
<div className="comment">
<h2 className="commentAuthor">
{ this.props.author }
</h2>
<span dangerouslySetInnerHTML={{ __html: rawMarkup }} />
</div>
);
}
});
// I manage the comment form.
var CommentForm = React.createClass({
// I validate the form data (lightly) and then submit the comment for
// processing.
handleSubmit: function( event ) {
event.preventDefault();
var author = React.findDOMNode( this.refs.author ).value.trim();
var text = React.findDOMNode( this.refs.text ).value.trim();
if ( ! author || ! text ) {
return;
}
// Pass the comment back up to the parent using the passed-in submission
// handler.
this.props.onCommentSubmit({
author: author,
text: text
});
React.findDOMNode( this.refs.author ).value = "";
React.findDOMNode( this.refs.text ).value = "";
},
// I render the view using the current state and properties collection.
render: function() {
return(
<form className="commentForm" onSubmit={ this.handleSubmit }>
<input type="text" placeholder="Your name" ref="author" />
<input type="text" placeholder="Say something..." ref="text" />
<input type="submit" value="Post" />
</form>
);
}
});
// Render the root CommentBox and mount it inside the given element.
React.render(
<CommentBox url="/comments.json" pollInterval={ 2000 } />,
document.getElementById( "content" )
);
</script>
</body>
</html>
Coming from an AngularJS background, I have to admit it was bit jarring to see all of these Global objects, just handing out there for the world to see. I don't know if that is how ReactJS works? Or, if this is done just to keep the tutorial simple? From what I have seen at work, it looks like ReactJS might lean on the CommonJS module pattern in order to manage the global scope.
Using inline HTML (through the JSX traspiling) is a very interesting approach. On the one hand, it seems like it would get unwieldy; but, on the other hand, maybe that's the point. By including your HTML in your JavaScript, you are, for sure, more likely to keep your views very small which will force you to favor component composition.
Ok, now that we see the ReactJS tutorial, here is my attempt to recreate the same thing in AngularJS. Since Angular doesn't [easily] allow for inline HTML (like JSX does), I've at least tried to group the Controllers and "text/ng-template" Views together. This way, you can see the Controller and then, below it, you can see the View for which it is managing state.
<!doctype html>
<html ng-app="Tutorial">
<head>
<meta charset="utf-8" />
<title>Hello AngularJS</title>
<script src="/vendor/angularjs/angular-1.3.16.min.js"></script>
<script src="/vendor/marked/marked-0.3.2.min.js"></script>
<script type="text/javascript">
angular.module( "Tutorial", [] );
</script>
</head>
<body>
<h1>
Hello World: Comparing ReactJS to AngularJS
</h1>
<div
bn:comment-box
url="/comments.json"
poll-interval="2000">
</div>
<script type="text/javascript">
// I manage the Comment Box.
angular.module( "Tutorial" ).directive(
"bnCommentBox",
function() {
// Return the directive configuration object.
return({
controller: Controller,
scope: {
url: "@",
pollInterval: "@"
},
templateUrl: "comment-box.htm"
});
// I manage the Comment Box view-model.
function Controller( $scope, $http ) {
// I am the collection of comments to render.
$scope.comments = [];
// Start polling the remote URL for new content.
setInterval( loadRemoteData, $scope.pollInterval );
// Load the initial data payload.
loadRemoteData();
// ---
// PUBLIC METHODS.
// ---
// I add a new comment to the collection.
$scope.submitComment = function( comment ) {
// Optimistically add the new comment to the collection before
// we even hear back from the server.
$scope.comments = $scope.comments.concat( comment );
var promise = $http({
method: "POST",
url: $scope.url,
data: comment
})
.then(
function handleResolve( response ) {
// Store the new comments collection, overwriting the
// existing reference.
$scope.comments = response.data;
},
function handleReject( response ) {
console.error( "Error", response );
}
);
};
// ---
// PRIVATE METHODS.
// ---
// I load the remote data from the server.
function loadRemoteData() {
var promise = $http({
method: "GET",
url: $scope.url,
cache: false
})
.then(
function handleResolve( response ) {
$scope.comments = response.data;
},
function handleReject( response ) {
console.error( "Error", response );
}
);
}
}
}
);
</script>
<script type="text/ng-template" id="comment-box.htm">
<div class="commentBox">
<h1>
Comments
</h1>
<div
bn:comment-list
comments="comments">
</div>
<div
bn:comment-form
on-submit="submitComment( comment )">
</div>
</div>
</script>
<script type="text/javascript">
// I manage the list of comments.
angular.module( "Tutorial" ).directive(
"bnCommentList",
function() {
// Return the directive configuration object.
return({
scope: {
comments: "="
},
templateUrl: "comment-list.htm"
});
}
);
</script>
<script type="text/ng-template" id="comment-list.htm">
<div class="commentList">
<div
ng-repeat="comment in comments track by $index"
bn:comment
author="comment.author"
text="comment.text">
</div>
</div>
</script>
<script type="text/javascript">
// I manage the individual comment.
angular.module( "Tutorial" ).directive(
"bnComment",
function( $sce ) {
// Return the directive configuration object.
return({
controller: Controller,
scope: {
author: "=",
text: "="
},
templateUrl: "comment.htm"
});
// I control the comment view-model.
function Controller( $scope ) {
$scope.markdown = "";
// Since the HTML is based on the content of the comment text, we
// need to watch the text for changes so that we can recompile the
// markdown when needed.
$scope.$watch(
"text",
function handleTextChange( newValue ) {
// NOTE: Since processing markdown is an "expensive" action,
// I want to get a sense of how often this will run.
console.log( "Running marked on comment." );
$scope.markdown = marked(
$scope.text,
{
sanitize: true
}
);
$scope.markdown = $sce.trustAsHtml( $scope.markdown );
}
);
}
}
);
</script>
<script type="text/ng-template" id="comment.htm">
<div class="comment">
<h2 class="commentAuthor">
{{ author }}
</h2>
<div ng-bind-html="markdown"></div>
</div>
</script>
<script type="text/javascript">
// I manage the comment form.
angular.module( "Tutorial" ).directive(
"bnCommentForm",
function() {
// Return the directive configuration object.
return({
controller: Controller,
scope: {
addComment: "&onSubmit"
},
templateUrl: "comment-form.htm"
});
// I manage the comment form view-model.
function Controller( $scope ) {
// Set up the form input value defaults.
$scope.form = {
author: "",
text: ""
};
// ---
// PUBLIC METHODS.
// ---
// I validate the form data (lightly) and then submit the comment
// for processing.
$scope.handleSubmit = function() {
var author = $scope.form.author.trim();
var text = $scope.form.text.trim();
if ( ! author || ! text ) {
return;
}
// Pass the comment back up to the parent using the passed-in
// submission handler.
$scope.addComment({
comment: {
author: author,
text: text
}
});
$scope.form.author = "";
$scope.form.text = "";
};
}
}
);
</script>
<script type="text/ng-template" id="comment-form.htm">
<form class="commentForm" ng-submit="handleSubmit()">
<input ng-model="form.author" type="text" placeholder="Your name" />
<input ng-model="form.text" type="text" placeholder="Say something..." />
<input type="submit" value="Post" />
</form>
</script>
</body>
</html>
The AngularJS version is a bit longer, mostly due to the directive definition objects which tell AngularJS how to translate the HTML attributes into state. But, overall, the code is basically the same.
One thing that is nice about the ReactJS version is that the rendered HTML more closely resembles the view you "intended" to build. In the AngularJS version, directive templates don't replace the element that references the directive; instead, the template content is appended to the root element. This isn't a problem, per say, it just makes the DOM a little bit heavier. That said, I think the benefit of the AngularJS approach is that it makes things like transition-animations a bit easier to reason about. It also means that you can easily add both structural and behavior directives to the same element through directive composition.
Since this getting started tutorial uses Marked, and parsing markdown is a relatively expensive process, I put in some logging to see how often it gets run. In the AngularJS version, it gets run once per comment when the comment is first encountered. In the ReactJS version, it gets run once per comment whenever the state is changed. This is because React's general philosophy is "always be rendering." That said, it looks like ReactJS has ways to mitigate expensive rendering operations through the shouldComponentUpdate() component lifecycle method.
At first glance, from a "getting started" perspective, I don't see much difference between the two frameworks (from a view-rendering standpoint). ReactJS uses the virtual DOM, AngularJS uses $watch() bindings; but, basically, they're doing the same thing. The biggest difference, to me, seems to be in how opinionated the frameworks are. ReactJS clearly favors immutable state. But, there's nothing that prevents you from using the same approach in AngularJS; it's just that AngularJS let's you go your own way if you want to. That said, I think this is also one of the biggest criticisms of AngularJS - that it doesn't "lead" the developer enough.
This just scratches the surface, so I can't really make any substantive comparison between the two frameworks. I know when you bring in things like Flux and server-side rendering, the differences may become more pronounced. More to come.
Want to use code from this post? Check out the license.
Reader Comments
Hi Ben,
Nice comparison without explicit judgement. Btw, It would be interesting to do the same comparison, but with the Angular 2.0 component syntax.
I also recently started with ReactJS, at first to understand the upcoming Angular 2.0 component paradigm (which seemed to be influenced by ReactJS). After the initial 'what is this weird looking JSX doing in my JavaScript' reaction, it kind of clicked. I like the fact that all parts of a component can be contained in a logical unit and that the mental context switch between code and markup is minimal.
Regards, Ronald.
Maybe most of the difference is regarding "how" they mutate the DOM. While Angular mostly replaces the entire node, React just apply the minimal changes. At least, that's what I understand.
Take for instance a change in a class attribute, while Angular will probably replace the entire node React will just apply a change in the class attribute. In the same way, imagine a structure like this:
[div class="node"]
[span class="subnode"][/span]
[h1]Other[/h1]
[p]things[/p]
[/div]
If for cause of any change in the state you need to remove the span (subnode), React will just remove that node directly, while Angular would probably just replace the entire div (node).
This makes apps written in React extremely fast when you have hundreds or thousand of elements, because the Virtual DOM is a tool that lets React know how to mutate the real dom in order to get what you coded in the virtual representation. Keep in mind that rendering a real DOM node is one of the most expensive tasks the browser have to deal with.
Sorry I had to use '[' and ']' for HTML tags... I could not submit my post using angle brackets
@Ronald,
I love AngularJS, so it's hard to fight back too much judgement :) But, I'm really trying to look at it honestly. In fact, this morning, I wanted to see what AngularJS looked like if it used something like the .setState() method that ReactJS does:
www.bennadel.com/blog/2858-what-if-angularjs-had-a-setstate-method-like-reactjs.htm
I know that AngularJS triggers a *lot* of digests; so, the .setState() method was a very interesting (to me) possibility when it comes to cutting down on the amount of work that Angular has to do.
I've only listened to things about AngularJS 2.0, so I haven't done any digging myself. I hear it is still changing a lot. Am looking forward to playing with it once it gets closer to release.
@Damian,
Sorry about that - I think I block A-tags (cause of high spamming); but, I don't see anything in your code that should have been problematic. I'll have to review the code.
That said, all of the DOM mutation is done by individual directives. So, there's no one way that it takes place. As much as possible, Angular tries to no remove DOM elements. This is why the ngRepeat tags has a "track by" aspect, in the same way that you can pass a "key={id}" to a React component. Both of them are using the data-driven key to try and keep a better Data->DOM relationship to reduce processing.
That said, something I read recently did point out that a lot of the React vs. Angular comparisons have large sets of data that DO NOT use the "track by" feature of the repeater. And, in that case, you are right - AngularJS will rip the DOM elements out and recreate them over and over (when data is refreshed) which is really bad for performance. So, as long as you're using the right techniques, Angular should do minimal DOM work.
I think once you start really playing with ReactJS and take a look at Flux, you will like them a lot.
Regarding the Isomorphic views, it's just the initial view that is rendered on the server, after that it goes back to acting like a normal SPA.
"One thing that is nice about the ReactJS version is that the rendered HTML more closely resembles the view you "intended" to build. In the AngularJS version, directive templates don't replace the element that references the directive; instead, the template content is appended to the root element"
Can't you just use: { replace: true } in your directive definition to replace your custom element with the directive template?
This is a good article.
I've been playing around and using AngularJs in my projects since a couple of years.
Thinking of getting started with React.js as its gaining quite a bit of popularity.
Aurelia is also another framework out there that is raising some eyebrows, need to look into it as well.
@Justin {replace: true} is (or is being) deprecated and has been "considered harmful"
@Hatem,
That's such an interesting idea! I keep mulling over AngularJS and the possibilities of Isomorphic JavaScript, but it seems like a very difficult problem to solve. I think the one reason that ReactJS can do it is because they are very strict about component views. From what I remember reading in the documentation, it seems there are 2 key aspects:
1. Views can only have a single root element.
2. When a view is rendered in an existing DOM node, any existing content is stripped out.
NOTE: Take the above with a grain of salt - I've only looked through the documentation once.
AngularJS has neither of these limitations - a view can have multiple top-level siblings and it will gladly work around existing content (your mileage may vary). As such, I think it's very difficult to know how to reverse engineer an AngularJS view that has been rendered elsewhere. I am not sure that it would be possible.
@Justin,
The {replace:true} aspect of directives is deprecated and will be removed in AngularJS 2.0. There might be another way to do it with Transcluding or something, but it is not clear in my head.
@Rahil,
Hopefully there will be some good cross-pollination of ideas and all of the frameworks will get better :)
@Fesh, @Justin,
I think replacing the top-level element can be tricky when you can have any number of directives on a single element. Plus, the animation probably becomes a lot harder to reason about. I think, even in ReactJS, if you want to animate stuff, you actually need to wrap your components in something that actually does the animation.... but, I don't recall off hand.
@Fresh, @Justin,
After commenting, I was curious about some of the reasons. Did some Googling and came across these Stack Overflow threads:
http://stackoverflow.com/questions/24194972/why-is-replace-deprecated-in-angularjs
http://stackoverflow.com/questions/28822194/alternatives-to-replacetrue-for-angularjs-directives
Looks like it is being deprecated mostly because it is harder to reason about and debug, which I think probably make sense.
@Ben,
Your column header "Dirty Data Checking" is misleading when it comes to React.
Render updates in React are normally in response to specific events that you, the developer, have set in your code. React has no equivalent to Angular's dirty checking (the source of so many of the latter's performance problems).
I'd like to read some of those Angular poems sometime. Perhaps a future blog post? Perhaps a future book? You could call it "Sonnets to $scope". Or "$http Haikus". Think about it.
@Brownieboy,
I am not sure I entirely understand what you mean. Neither ReactJS nor AngularJS do any dirty-data checking unless there is some event triggered by the code, which is usually triggered by a user-interaction.
I think maybe what you mean is that even IF a user triggers an action, ReactJS won't actually do any dirty data checking UNLESS the click-handler calls .setState() or .forceUpdate()? Is that what you mean? In contrast, AngularJS will always trigger a digest in response to an ngClick directive, regardless of whether of nor anything in the view-model has actually changed.
If that's what you mean, I think you are right, in theory. But, in practicality, I am not sure how often a user-event runs without actually causing a state change. After all, what would be the point in having a hook to the user-event if it didn't change the state.
That said, in an AngularJS app, you can easily create user-event handlers that don't trigger a digest. You just need to create a directive that does what you want.
@Cameron,
Ha ha, I'll be happy to come up with AngularJS poems. In the meantime, here's one about jQuery:
www.bennadel.com/blog/2598-years-later-i-still-love-jquery.htm
... set to song by the great Matthew Hopson :D
@Ben
React doesn't depend on globals. When I got started on react, I invested the time to find a JSX require.js plugin. You can see my demo app at https://github.com/gregturn/spring-a-gram/blob/master/spring-a-gram-frontend/src/main/resources/static/app/images.jsx.
Hello Ben,
Thanks for your article. I would also mention about reactjs:
1) react ability to server-rendering
2) you can learn and be effective in react much quicker than angular. You use more vanilla JS in React :-) which lower the learning-curve vs. "angular way of writing javascript"
I would consider anyone comparing both, to read all opinions from people who done projects in both angular & react: https://www.quora.com/Is-React-killing-Angular this article is quite popular and engaged over 30 developers who discuss both technologies :-)
I can't wait to Angular2 - and see the how it compares to other existing solutions. Ben, do you plan to write about angular2 vs. reactjs ?
Regards,
Kamil
@Ben,
I've trying to get my head wrapped around React and I don't really care for the component style yet... Maybe I just need to be a bit more open-minded.
@Greg,
That demo link has an extraneous period in the link but seeing that I'm quite familiar with Spring, I'm definitely going to check it out to see how you implemented the entire stack...
@Edward,
When you dig into AngularJS, there are many analogs to the component and the component life-cycle. But, one thing that is really nice in ReactJS is the component life-cycle method, componentDidUpdate(), which is called after the virtual DOM changes are flushed to the DOM.
This is something that is never quite clear in AngularJS. Yes, the link() function is always fired after the DOM is mounted. But, subsequent changes are sometimes fuzzy, especially because so much of the DOM updating is bound to $watch() statements, which are part of the same life-cycle. As such, in AngularJS, there is a lot of $evalAsync() to ensure that the DOM is ready to be queried.
It's frustrating at first, when you don't know what's going on. But, eventually you see the patterns and you just have to adjust accordingly. But, the clarity in the ReactJS life-cycle is very nice.
@Kamil,
I have to say, I hear a lot of people say that ReactJS is more "vanilla" JavaScript or that it's "closer to the metal." I don't necessarily see this since it seems like ReactJS lets people use just as many (if not more) 3rd-party libraries though things like Browserify and WebPack. I mean, ultimately, everything is just JavaScript objects and functions. It just so happens that in AngularJS, they are managed by a dependency-injection framework, where as in ReactJS, you basically are left to your own devices.
But, I'm learning a lot of ReactJS. It really has forced me into a whole new mindset. Learning more about ReactJS is probably one of the most helpful / important things that I've done lately, even if I continue to use AngularJS long-term.
As far as AngularJS 2, I'll definitely be checking it out. So far, I've only listened to podcasts, though, no actual coding. It sounds like I'm going to have bone up on ES6 and things like Babel and WebPack to be able to actually start using AngularJS 2 - so that will probably be another boost in my general programming understanding.
Thanks for the link - looks interesting. I will check it out.
@Greg,
I _just_ started trying to wrap my head around using require() in client-side code. Was playing around with WebPack and compiling things using Babel. Right now, I am just in the "hello world" state. But, it seems super interesting.
I used to work with John Hann, the maintainer of curl.js, and was hence catapulted into a life of good modular JS.
React Native is sharing similar design concept of ReactJS. It is another benefit/reason of starting building solutions on ReactJS.
Great!
Thanks for the Article.
This was a good read! Thank you for sharing your thoughts. I found this interesting blog that may be helpful , check it out https://goo.gl/fuLyNh