The React Pyramid

Within Delve we try and enforce three kinds of React components: Containers, Smart, and Dumb. This differentiation is largely inspired by Dan Abramov. Our React components are organized in a hierarchy like so:

React Component Hierarchy

At the top sits React Router, which is typically how a React app will manage routes or pages. For Delve, we have a notion that each route corresponds to a Container component, which is a relatively light weight construct. Containers are primarily responsible for ensuring that the right Smart components are on the page and potentially passing down route parameters, such as the selected user, query text and so forth. Container components render little real DOM; they mainly return one or more Smart components in their render() function. Examples of Containers for Delve include the HomePage, UserPage, and SearchPage.

Smart components are named so because they’re the components permitted to talk to Stores and Action Creators in our Flux pattern. These components are entirely application specific and very often scenario and/or UX specific. Smart components register with the Stores that they need data from, receive update notifications from Stores and convert the Store data into props for the Dumb components that they render. Smart components in our pattern use setState() and return Dumb components from the render() function by using the state property. Examples of Smart components are HomeFeed, ModifiedFeed, RelatedPeople and the SearchBox.

Dumb components make up the majority of our application and are entirely reusable inside and outside of the Delve app. These components know nothing about Stores, Actions, and Action Creators, or anything about the rest of the app. They are dumb because they only know about the props interface and types that they are passed. Dumb components are where we render the majority of our DOM and define CSS for a component. We try to think about Dumb components as nearly entirely functional and stateless, although there are small exceptions where a dumb component needs to hold state outside of a Store to perform its job. Dumb components will usually form a dumb component hierarchy by referencing and render()-ing other dumb components. By enforcing the rule that Dumb components are only allowed to reference props, we ensure that the majority of our Dumb components are reusable and easily refactorable for future users. Examples of Dumb components are FavoritesButton, PersonImage and CardLinkArea.

The Delve app consists of ~90% Dumb components. This means that our app-specific logic is contained within a small number of components/files, which makes refactoring and reuse across the app extremely easy and flexible.

We also have fantastic debugging capabilities with this hierarchy: it’s quite easy to use the React F12 tools to inspect a component and make a simple determination about where the problem is. If the props are correct, then we should look at the Dumb component’s render() function. If not, then we walk the component hierarchy, up to the Smart component and eventually to the Smart component’s state. If the Smart component’s state is wrong, then something is funky in the Store or the Smart component’s setState() function.

Dumb Component Best Practices

One thing that we learned quickly after shipping our first ~50 React components is that you can’t ensure that all render() functions will execute successfully. No matter how good your type enforcement or your unit testing strategies are, you will run into some exceptions in production. When a render() function throws an exception, it will bubble up and prevent potentially large sections of your app from rendering. We quickly decided that we needed a solution that accomplished two goals:

Ensure that a bug in a React component, deep in the tree, doesn’t cause the entire page to fail. Log the failure for error analysis and debugging.

To address these two goals we introduced a BaseComponent class into our code, that all components inherit from. We enforce the inheritance pattern via a custom tslint rule. It seems possible to do this via a decorator as well, but we haven’t found time to investigate that. Let us know if you’re doing something similar!

In addition to the above error catching, we also enforce naming conventions via code reviews. Specifically, we try and look for scenario specific names that should be more generic to encourage reusability. For example, instead of having a bool property on a Card for “isSearching”, we would try and use a more descriptive and reusable name such as “showHitHighlightedText.”

More Smaller Components

A common mistake we catch in code review is developers putting too much stuff into a single component. This seems to be a common anti-pattern:

There are two reasons we try to avoid this anti-pattern. The first is that SomeComponent will grow to be a very big component that is hard to read. The second reason is that we’re overexposing this.props and it’s IGiganticPropList interface to multiple functions that don’t need access to the entire interface. Together these make this component harder to reuse and refactor than it should be. With React 0.14, functional components make this very easy to get right from the start:

Using more smaller components encourages us to define the correct props interfaces, increases reusability and readability. When we see renderXYZ or getXYZ in a code review, it’s a clear signal that we should be creating a new React component instead of a helper function.

Smart Component Best Practices

Our Smart components are designed to make Store and React life-cycle management easy. All Smart components inherit from a base SmartComponent class that provides this support. To define a Smart component in our pattern, you specify which Stores you’ll get data from in the constructor and then override a getState() function that defines how your Smart component will transform Store data into internal state.

In addition to the above, a Smart component will of course need to define a render() function that transforms the internal state members into Dumb components.

One gotcha that you may come across in how your Smart components work is related to the ActionCreator.Dispatch(Action) -> Store.EmitChange() -> SmartComponent.SetState() call pattern. This pattern makes sense when there is a single Store responding to an Action, but things can quickly become complex when multiple Stores listen for an Action, change their internal state, and emit change. What may happen is that your SmartComponents may end up setting state (and rendering) multiple times as an Action is processed sequentially by multiple stores.

Many Flux frameworks solve this pattern for you. Redux is one example. In Redux the entire reducer tree is evaluated at once and only then are components allowed to read from the tree. Delve solves this problem in our Flux pattern by hooking into the Dispatcher and only allowing the Smart Components to setState after all Stores have had a chance to process the action. You could also debounce and/or throttle your Smart Components, which may be very useful if you have many Actions constantly being processed.

Flux Dispatching and Store Processing

Just as React components can have bugs in their render() functions, which cause a cascading break due to unhandled exceptions, Stores can have the same kind of bug. To limit the impact of this and allow us to diagnose these bugs in production we have a similar base class for our Stores that performs the try/catch/log logic:

Many Flux frameworks have a similar concept and mechanism, be sure to double check! One nice thing about our TypeScript typed actions is that we can easily log which action was being processed and caused the exception.