Thinking In React In AngularJS

I love AngularJS. But, I've been digging into ReactJS a lot lately in order to both get more perspective on application development as well as to be able to help my React-using teammates when they encounter problems. So far, I've found the process to be quite fruitful especially when it comes to the cross-pollination of ideas (in both directions). As a fun experiment, I wanted to see what it would feel like to repurpose the "Thinking in React" documentation, by Pete Hunt, but for an AngularJS context. Meaning, what if I took that article and made it about AngularJS, not ReactJS, while trying to apply all of the same principles.

Before we start, I would like to say that this is definitely the direction that my AngularJS code is moving. However, I don't break my AngularJS code down into arbitrarily small components. AngularJS and ReactJS have different strengths. And, in my opinion, I think AngularJS is better about keeping larger chunks of markup easier to reason about, especially with directives like ngRepeat.

=============

CAUTION: So beginneth the experiment. I did have to leave an entire section out because I couldn't figure out how to translate it in a meaningful way.

=============

Thinking in React

(as bastardized by Ben Nadel)

AngularJS is, in my opinion, the premier way to build big, fast Web apps with JavaScript. It has scaled very well for us at InVision App, Inc.

One of the many great parts of AngularJS is how it makes you think about apps as you build them. In this post, I'll walk you through the thought process of building a searchable product data table using AngularJS.

Start with a mock

Imagine that we already have a JSON API and a mock from our designer. Our designer apparently isn't very good because the mock looks like this:

The first thing you'll want to do is to draw boxes around every component (and subcomponent) in the mock and give them all names. If you're working with a designer, they may have already done this, so go talk to them! Their Photoshop layer names may end up being the names of your React components!

But how do you know what should be its own component? Just use the same techniques for deciding if you should create a new function or object. One such technique is the single responsibility principle, that is, a component should ideally only do one thing. If it ends up growing, it should be decomposed into smaller subcomponents.

You'll see here that we have three components in our simple app.

FilterableProductTable (orange): contains the entirety of the example.

SearchBar (blue): receives all user input.

ProductTable (green): displays and filters the data collection based on user input.

Ben's Note: With AngularJS, you have to take on more of a "shadow-DOM" mindset since replacing an entire element isn't an obvious task. In most cases, this is fine; however, with Tables, we have to make some concessions since we need to generate valid HTML table markup. That said, unlike Pete's example, I would not try to break up the actual table into smaller components - to me, the table itself is a "single responsibility."

Now that we've identified the components in our mock, let's arrange them into a hierarchy. This is easy. Components that appear within another component in the mock should appear as a child in the hierarchy:

Now that you have your component hierarchy, it's time to implement your app. The easiest way is to build a version that takes your data model and renders the UI but has no interactivity. It's best to decouple these processes because building a static version requires a lot of typing and no thinking, and adding interactivity requires a lot of thinking and not a lot of typing. We'll see why.

To build a static version of your app that renders your data model, you'll want to build components that reuse other components and pass data using attributes. Attributes are a way of passing data from parent to child. We can use state for this step; but, let's try to keep state management to a minimum.

Ben's Note: While the ReactJS version of this can avoid state entirely because it does all of the data transformation within the render() method, I do use state to create an intermediary data structure that is easier to render. I could have moved all of that processing into a method call that gets run on each digest; but, this feels expensive and I tend to expose properties over methods.

At the end of this step, you'll have a library of reusable components that render your data model. The component at the top of the hierarchy (FilterableProductTable) will take your data model as an attribute. At this point, we can run the initial render of the components; but, the components will not be updated with the underlying data model.

Ben's Note: If we did not have intermediary data structures, the views would automatically update at this point. But, this particular application does not lend itself well to that.

Ben's Note: I couldn't really translate this section into an AngularJS context because the approaches, while similar, are too different for this particular application. I ended up creating more state than you may normally in order to 1) Make the rendering easier and 2) prevent the ngModel binding from directly altering the props being passed-in via the isolate-scope binding.

// criteria changes, we have to synchronize the transformed structure.

// --

// NOTE: Watch configuration will set initial values.

$scope.$watchCollection(

"[ props.products, props.filterText, props.inStockOnly ]",

function handlePropsChange() {

vm.categories = getFilteredProducts(

props.products,

props.filterText,

props.inStockOnly

);

}

);

// ---

// PRIVATE METHODS.

// ---

// I return the category breakdown for the given products after

// the given filtering has been applied.

function getFilteredProducts( products, filterText, inStockOnly ) {

var categories = [];

var category = null;

var lastCategory = null;

filterText = filterText.toLowerCase();

products

// Filter out the products that don't match the current criteria.

.filter(

function operator( product ) {

// Filter based on text.

if (

filterText &&

( product.name.toLowerCase().indexOf( filterText ) === -1 )

) {

return( false );

}

// Filter based on stock status.

if ( inStockOnly && ! product.stocked ) {

return( false );

}

// If we made it this far, the product was not

// filtered-out of the results.

return( true );

}

)

// Now that we have the filtered products, break them

// down into the different categories. And, since we pre-

// filtered the products, we know that we won't get any

// empty categories in the final breakdown.

.forEach(

function iterator( product ) {

if ( product.category !== lastCategory ) {

category = {

name: ( lastCategory = product.category ),

products: []

};

categories.push( category );

}

category.products.push({

name: product.name,

stocked: product.stocked,

price: product.price

});

}

)

;

return( categories );

}

},

controllerAs: "vm",

restrict: "E",

scope: {

products: "=",

filterText: "=",

inStockOnly: "="

},

template:

`

<table>

<col width="80%" />

<col width="20%" />

<thead>

<tr>

<th>

Name

</th>

<th>

Price

</th>

</tr>

</thead>

<!-- If we have data to display. -->

<tbody

ng-if="vm.categories.length"

ng-repeat="category in vm.categories track by category.name">

<tr>

<th colspan="2">

{{ category.name }}

</th>

</tr>

<tr

ng-repeat="product in category.products track by product.name"

ng-class="{ 'out-of-stock': ! product.stocked }">

<td>

{{ product.name }}

</td>

<td>

{{ product.price }}

</td>

</tr>

</tbody>

<!-- If we have no data to display. -->

<tbody ng-if="( ! vm.categories.length )">

<tr>

<td colspan="2" class="no-data">

<em>No products match your criteria.</em>

</td>

</td>

</tbody>

</table>

`

});

}

);

</script>

</body>

</html>

OK, so we've identified what the minimal set of app state is. Next, we need to identify which component mutates, or owns, this state. It may not be immediately clear which component should own what state. This is often the most challenging part for newcomers to understand, so follow these steps to figure it out:

For each piece of state in your application:

Identify every component that renders something based on that state.

Find a common owner component (a single component above all the components that need the state in the hierarchy).

Either the common owner or another component higher up in the hierarchy should own the state.

If you can't find a component where it makes sense to own the state, create a new component simply for holding the state and add it somewhere in the hierarchy above the common owner component.

Let's run through this strategy for our application:

ProductTable needs to filter the product list based on state and SearchBar needs to display the search text and checked state.

The common owner component is FilterableProductTable.

It conceptually makes sense for the filter text and checked value to live in FilterableProductTable

Cool, so we've decided that our state lives in FilterableProductTable. First, let's define the filterText and inStockOnly on the view-model to reflect the initial state of your application. Then, pass filterText and inStockOnly to ProductTable and SearchBar as an attribute. Finally, use these attributes to filter the rows in ProductTable and set the values of the form fields in SearchBar.

You can start seeing how your application will behave: set filterText to "ball" and refresh your app. You'll see that the data table is updated correctly.

// criteria changes, we have to synchronize the transformed structure.

// --

// NOTE: Watch configuration will set initial values.

$scope.$watchCollection(

"[ props.products, props.filterText, props.inStockOnly ]",

function handlePropsChange() {

vm.categories = getFilteredProducts(

props.products,

props.filterText,

props.inStockOnly

);

}

);

// ---

// PRIVATE METHODS.

// ---

// I return the category breakdown for the given products after

// the given filtering has been applied.

function getFilteredProducts( products, filterText, inStockOnly ) {

var categories = [];

var category = null;

var lastCategory = null;

filterText = filterText.toLowerCase();

products

// Filter out the products that don't match the current criteria.

.filter(

function operator( product ) {

// Filter based on text.

if (

filterText &&

( product.name.toLowerCase().indexOf( filterText ) === -1 )

) {

return( false );

}

// Filter based on stock status.

if ( inStockOnly && ! product.stocked ) {

return( false );

}

// If we made it this far, the product was not

// filtered-out of the results.

return( true );

}

)

// Now that we have the filtered products, break them

// down into the different categories. And, since we pre-

// filtered the products, we know that we won't get any

// empty categories in the final breakdown.

.forEach(

function iterator( product ) {

if ( product.category !== lastCategory ) {

category = {

name: ( lastCategory = product.category ),

products: []

};

categories.push( category );

}

category.products.push({

name: product.name,

stocked: product.stocked,

price: product.price

});

}

)

;

return( categories );

}

},

controllerAs: "vm",

restrict: "E",

scope: {

products: "=",

filterText: "=",

inStockOnly: "="

},

template:

`

<table>

<col width="80%" />

<col width="20%" />

<thead>

<tr>

<th>

Name

</th>

<th>

Price

</th>

</tr>

</thead>

<!-- If we have data to display. -->

<tbody

ng-if="vm.categories.length"

ng-repeat="category in vm.categories track by category.name">

<tr>

<th colspan="2">

{{ category.name }}

</th>

</tr>

<tr

ng-repeat="product in category.products track by product.name"

ng-class="{ 'out-of-stock': ! product.stocked }">

<td>

{{ product.name }}

</td>

<td>

{{ product.price }}

</td>

</tr>

</tbody>

<!-- If we have no data to display. -->

<tbody ng-if="( ! vm.categories.length )">

<tr>

<td colspan="2" class="no-data">

<em>No products match your criteria.</em>

</td>

</td>

</tbody>

</table>

`

});

}

);

</script>

</body>

</html>

So far, we've built an app that renders correctly as a function of attributes and state flowing down the hierarchy. Now it's time to support data flowing the other way: the form inputs deep in the hierarchy need to update the state in FilterableProductTable.

While AngularJS makes two-way data binding possible, we're going to make it explicit in this demo so that one component doesn't end up mutating data that it doesn't "own."

Let's think about what we want to happen. We want to make sure that whenever the user changes the form, we update the state to reflect the user input. Since components should only update their own state, FilterableProductTable will pass a callback to SearchBar that will fire whenever the state should be updated. We can use the $watchCollection() binding on the inputs to be notified of it.

Though this sounds complex, it's really just a few lines of code. And it's really explicit how your data is flowing throughout the app.

And that's it

Hopefully, this gives you an idea of how to think about building components and applications with AngularJS. While it may be a little more typing than you're used to, remember that code is read far more than it's written, and it's extremely easy to read this modular, explicit code. As you start to build large libraries of components, you'll appreciate this explicitness and modularity, and with code reuse, your lines of code will start to shrink. :)

I am the co-founder and lead engineer at InVision App, Inc — the world's leading prototyping,
collaboration & workflow platform. I also rock out in JavaScript and ColdFusion 24x7 and I dream about
promise resolving asynchronously.