Free lesson · React Deep Dive
Why React Exists: The Pre-React Frontend
Why React Exists: The Pre-React Frontend
To understand React, you have to understand the world it was born into. Most engineers under 30 have no memory of it; many engineers over 35 actively try to forget it. The frontend in 2010-2012 was a particular kind of mess - not because the engineers were bad, but because the tools weren't built for what the work had become.
I came into the industry in 2018, after React already existed. But my first job was at an agency in Warsaw, maintaining client codebases that were five, eight, ten years old. So I encountered the pre-React era as archaeology - opening Backbone.js and Knockout.js and AngularJS 1 codebases that real teams had shipped in 2012 and were still running. The patterns those codebases used aren't relics from some unrelated discipline. They're the immediate ancestors of what we do now. Understanding why they didn't scale is the cleanest path to understanding why React does.
This lesson covers the world before React: what tools people used, what mental model those tools imposed, and what specifically broke at scale.
The Era
Roughly 2008-2013. Web applications were getting ambitious - Gmail had been around since 2004 showing what was possible; Google Maps was launched in 2005; Facebook was scaling rapidly; the iPhone shipped in 2007 making mobile web a real concern; HTML5 was being standardized. The browser had become a serious application platform, and engineers were building serious applications in it.
The tools they had were:
jQuery. Released 2006. By 2010 it was on something like 60% of all websites. Its job was to paper over browser inconsistencies and make DOM manipulation tolerable. It did this well. It did only this - jQuery has no application architecture, no state management, no rendering model. It's a DOM library.
Backbone.js. Released 2010 by Jeremy Ashkenas. Brought "MVC for the browser" - Models, Views, Collections, Router. The first widely-adopted application framework for the browser. Hugely influential; the codebase you'd find at a real company in 2012 was probably Backbone-based.
Knockout.js. Released 2010. Brought "two-way data binding" - declare a binding between a DOM element and a JavaScript property, and Knockout syncs them automatically. Felt magical at the time.
AngularJS (1.x). Released 2010 by Google. Brought two-way data binding plus a bigger framework around it - dependency injection, directives, the digest cycle. Got serious adoption around 2013-2014.
Ember.js. Released 2011. The most "framework-y" of these - a complete opinionated stack with conventions about routing, templates, models, the whole thing.
What every one of these had in common is the same approach to the actual UI work: you grab DOM elements and manipulate them imperatively. Sometimes through a binding layer that did the manipulation for you; sometimes by writing the manipulation directly. The mental model was that the DOM was the source of truth, and your JavaScript reached in to change it.
The Imperative Style
Let me make this concrete. Say you want to build a comment list. A user types a comment, hits enter, and the comment appears in the list. In jQuery, the code is something like:
$('#comment-form').on('submit', function (e) {
e.preventDefault();
var text = $('#comment-input').val();
$('#comment-list').append('<li>' + escapeHtml(text) + '</li>');
$('#comment-input').val('');
});
Read it carefully. Notice what you're doing:
- Find the form element.
- Listen for submit events.
- Find the input element. Read its value.
- Find the list element. Append a new list item with the text inside it.
- Find the input again. Clear it.
You're telling the browser step by step how to update the DOM. Every step is a DOM operation. Every operation can fail in subtle ways - what if #comment-list doesn't exist yet? What if the text contains HTML you forgot to escape? What if the user submits twice quickly and the order races?
For a list this small, fine. The example works. The problem isn't the example. The problem is what happens when you have fifteen of these handlers, and they share state, and they interact.
The Scaling Problem
Now imagine the comment isn't just text. It has a name, a timestamp, a like count, a "is this from the current user?" indicator. You need an edit button that turns the comment into an editable input. You need a like button that increments the count. You need a delete button. The user can be logged in or not, and the UI looks slightly different in each case. The list updates in real time when other users add comments.
Each of these interactions, in the imperative style, is a separate handler that reaches into the DOM and modifies things. The handlers share state - the current logged-in user, the current list of comments, whether there's a pending edit. They share assumptions about the DOM's structure - "the edit button is inside the comment li; the like count is in .like-count; the input becomes visible when you click edit."
What goes wrong:
State drifts. The DOM says one thing; your JavaScript variables say another. A bug fixes one but forgets to update the other. Two months later someone wonders why the like count occasionally shows a different number than the data.
Handlers tread on each other. The edit handler shows the input. The delete handler hides the comment. If a user clicks edit and then delete in quick succession, what happens? Depends on which handler ran last, which depends on event order, which is browser-dependent.
Initial state and live state diverge. The page loads with some initial HTML rendered by the server. Then the JavaScript wires up handlers on top. If a user updates a comment, the JavaScript updates the DOM. But the server's "initial render" still doesn't know about that update - refresh the page and you might lose state.
Updates trigger cascades. Updating one piece of UI requires updating five other pieces that depend on it. You can't easily know which pieces depend on what. The dependency graph lives in the heads of whoever wrote the original code, and as the codebase grows past one engineer, the graph becomes unknowable.
For applications below a certain size, this is tolerable. jQuery codebases for marketing sites or simple admin panels work fine. The problems show up in real applications with many interacting features.
What Backbone Tried
Backbone's contribution was structure. Instead of jQuery's "manipulate the DOM directly," Backbone said: keep the data in models, keep the rendering logic in views, and let views listen for model changes.
var Comment = Backbone.Model.extend({});
var CommentView = Backbone.View.extend({
template: _.template($('#comment-template').html()),
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}
});
This is better. When the model changes, the view re-renders itself. There's a notion of a model as the source of truth. The DOM is derived from the model.
But Backbone re-renders the entire view on every change. For a single comment that's fine; for a list of 200 comments where one changed, you're re-rendering all 200. So in practice you write custom render methods that only update the changed bits, which means you're back to imperative DOM manipulation, just with more ceremony.
And Backbone gives you no opinion about how views compose. You have a CommentView and a CommentListView. The list view contains the comment views. How do they communicate? Whatever you cook up. Real Backbone apps developed elaborate patterns for this - events, mediators, custom view hierarchies - and the patterns varied team to team.
What Knockout and Angular 1 Tried
Knockout and Angular 1 took a different angle: two-way data binding. Declare a binding between a DOM element and a JavaScript variable; the framework syncs them.
In Knockout:
<input data-bind="value: commentText" />
<button data-bind="click: addComment">Add</button>
<ul data-bind="foreach: comments">
<li data-bind="text: $data"></li>
</ul>
function ViewModel() {
this.commentText = ko.observable('');
this.comments = ko.observableArray([]);
this.addComment = function () {
this.comments.push(this.commentText());
this.commentText('');
}.bind(this);
}
ko.applyBindings(new ViewModel());
This is much shorter than the jQuery version. The view is declarative - it describes what the DOM should look like given the data. When the data changes, Knockout updates the DOM. When the user types in the input, Knockout updates the data.
You can see where React is going from here. The Knockout vision was the right direction. But Knockout (and Angular 1's similar binding system) had specific problems:
The bindings are stringly-typed. data-bind="text: $data" is a string. If you typo it, you don't find out until runtime. If you rename the property in the JavaScript, the string doesn't update.
Two-way binding gets confusing at scale. When data flows both directions through bindings, tracing where a value comes from becomes hard. Did the user type it? Did some other handler set it? Did some computed observable derive it? The graph of who-affects-whom becomes opaque.
Performance gets weird. Angular 1's "digest cycle" - the mechanism that propagates changes - would sometimes run multiple times to stabilize, and would sometimes miss changes entirely. Real Angular 1 apps had bugs that looked like "this value didn't update" and the fix was usually $scope.$apply() calls sprinkled in mystifying places.
No clear unit of UI composition. Both frameworks let you define directives and components, but the rules were complex and the patterns varied wildly.
What Was Actually Missing
Look at this list of pre-React tools. They each solved part of the problem. None solved all of it. What was missing was:
A coherent unit of UI. A component you can write, test, reuse, and reason about in isolation. Backbone had Views; Angular had directives; Ember had components. Each was different and none was great.
A clean rendering model. Some way to describe what the UI should look like, and have the framework figure out how to make the DOM match. Knockout and Angular tried; the bindings approach had real limits.
A unidirectional data flow. Some way to make it easy to trace where any value in the UI came from. Two-way binding made this systematically hard.
A model for component composition. Some way to build big UIs from small components without elaborate ceremony about how they communicate.
In 2010-2012 Facebook was hitting these limits painfully. The News Feed was getting more complex - comments, likes, reactions, embeds, "X people are talking about this," notification badges that needed to update everywhere. Each new feature was a battle with the framework. Engineers were spending more time on plumbing than on features.
That's the context React was born into. Not a clean slate - a deeply messy industry full of partial solutions that almost worked. The opportunity was visible to anyone who looked carefully; the question was what to do about it.
Next lesson: what Jordan Walke and the Facebook team actually did.
What I Want You to Take From This Lesson
Five things.
One: The pre-React frontend was imperative. You manipulated the DOM step by step. State lived in the DOM and in your variables, and the two drifted.
Two: At small scale this was fine. At Facebook scale it was untenable.
Three: Backbone, Knockout, Angular 1 each saw part of the problem. Each contributed ideas React would build on. None solved the whole.
Four: The missing piece was a coherent component model - a unit of UI you could compose and reason about.
Five: React didn't come from nowhere. It came from a specific industry frustration that many people were noticing at the same time. Several solutions emerged; React was one of them and turned out to be the most influential.
Try It
Exercise: Warmup (15 minutes). Find an old jQuery codebase (your old projects count). Open one of the more complex files. Trace through how state lives in it. Notice how many places the same piece of data is touched.
Exercise: Core (20 minutes). Implement the comment-list example from this lesson in jQuery, then in Knockout (or vanilla Backbone). Make it work. Note what feels awkward in each.
Exercise: Core (15 minutes). Read Jeremy Ashkenas's original Backbone announcement post. Note how he frames the problem Backbone solves; compare to how you'd frame it now.
Exercise: Stretch (45 minutes). Read Pete Hunt's "Rethinking Best Practices" talk transcript. It was the talk that convinced many engineers React was worth taking seriously in 2013. Notice what assumptions he challenges.