Sift Science has been using React in production for almost a year now. In that time, we grew our application from a Backbone + React frankenstein hybrid app into one fairly large hierarchy of React components. In this post, we’ll describe some techniques and best practices that have helped us scale our UI code base with minimal friction. We’ll also walk through some common component design patterns.
Hopefully this post will save you time and sanity and provide you with some new tools for maintaining a React code base that builds on itself (instead of breaking down) as the complexity of the UI grows.
Do more in componentDidUpdate
React is all about turning the imperative task of updating the DOM into a declarative one. It turns out that declaring other types of imperative behavior as a function of props and state can be beneficial as well. Here’s an example:
Let’s say that we’re building an interface to view and edit contacts. In the screenshot to the right, “contact #3” has unsaved changes. We would like the form to automatically save if the user navigates to another contact, say Contact #2, at this point.
A reasonable way to implement this functionality would be to have a method on our main component that looks like this:
navigateToContact: function(newContactId) { if (this._hasUnsavedChanges()) { this._saveChanges(); }
this.setState({ currentContactId: newContactId }); } … navigateToContact(‘contact2’)
This setup is kind of fragile though. We have to make sure that the click handlers for both the “contact #2” sidebar item and the “< prev contact” link in the bottom left corner use the navigateToContact method instead of directly setting the currentContactId state.
Here’s what it would look like if we implemented this declaratively using componentDidUpdate.
componentDidUpdate: function(prevProps, prevState) { if (prevState.currentContactId !== state.currentContactId) { if (this._hasUnsavedChanges()) { this._saveChanges(); } } }
In this version, the functionality to save the previous contact when navigating to a new one is baked into the component lifecycle. It’s much harder to break now since all event handlers can directly call this.setState({currentContactId: ‘contact2’}) instead of having to know to use a special method.
This example is oversimplified of course. Invoking navigateToContact in both event handlers doesn’t seem too bad here, but the problem becomes more apparent as the component grows in complexity. Declaring actions to be invoked based on prop and state changes will make your components more autonomous and reliable. This technique is especially useful for components that manage a lot of state, and it has made refactorings much more pleasant for us.
Taking composition to the max
Building a robust, maintainable, and highly composable library of components makes building your controller components much easier. In their Thinking in React tutorial, the authors recommend using the single responsibility principle as the basis for deciding what gets to be its own component.
An example of this in our code base is our Slidable component. It slides its children in and out and that’s it. Although it may seem like we’re going overboard with the single responsibility principle, it’s actually a big time saver because this thing is really good at sliding stuff. It can slide elements from any direction and use either edge as an anchor. It can use a JS transition instead of the default CSS one if that’s what the parent needs. And it’s also cross-browser compatible and unit tested. (So there’s more to the implementation than simply using CSSTransitionGroup.) Having this building block allows us to not really worry about the details of sliding stuff in components like Accordion or NotificationCenter, our Growl-like notification system.
Splitting up our components to be more reusable makes our team more productive, enforces a consistent look and feel, and lowers the barrier of entry for people who are not on the frontend team to make UI contributions. The following sections contain tips for building components with composability in mind.
State ownershipBump it up
Here’s another must-read section of the React docs. It advises us to keep our components as stateless as possible. If you find yourself duplicating/synchronizing state between a child and parent component, then move that state out of the child component completely. Have the parent manage state and pass it in as a prop to the child.
Consider a Select component, a custom implementation of the HTML <select> tag. Where should the “currently selected option” state live? The Select component usually represents some kind of data in the outside world such as the value of a specific field in a model. If we make a state called selectedOption live in the Select component then we would have to update both the model and the selectedOption state when the user chooses a new option. This kind of state duplication can be avoided by making Select accept a prop called selectedOption from the parent instead of managing it’s own state.
Intuitively it makes sense that this state belongs on the model, since that’s what the component represents. The Select component itself is just a (mostly) stateless UI control and the model is the backend. Select is mostly stateless because it can actually contain one piece of state: whether or not it is currently expanded. This state can live directly on Select because it’s a UI detail and usually not something that the parent is concerned with. In the next section we will demonstrate how the isCurrentlyExpanded state can be delegated to an even lower level component to keep true to the single responsibility principle.
Separating UI details from interaction logic
We have been using the pattern of stateful higher-level components and stateless lower-level components. The stateless components provide reuse of UI rendering details, styles, and markup. The stateful wrapper components provide reuse of interaction logic. This pattern has become our single most important rule to follow for keeping our components composable. Here’s a breakdown of how we built the Select component and how it reuses UI code that’s also used for the tooltip (TooltipToggle) component.
Select
The Select component is analogous to the <select> HTML tag. It accepts props such as a list of available options and the currently selected option, but it does not own any state. Not even a state to indicate whether or not it is currently expanded. Select is composed of DropdownToggle which handles dropdown expanded/collapsed state.
DropdownToggle
This component takes in a trigger element and children that will be displayed in a dropdown HoverCard when the trigger is clicked. Select passes a button with a downward facing arrow icon as the trigger into DropdownToggle. It also passes a selectable list of options as the children into DropdownToggle.
TooltipToggle
TooltipToggle is similar in scope to DropdownToggle because it accepts a trigger component and manages state to determine whether to show its children in a HoverCard. The difference is in how it decides to show the HoverCard; the interaction logic is different. While the DropdownToggle listens for clicks on the trigger element, the TooltipToggle listens for mouse hover events. It also doesn’t close when the ESC key is pressed, but the DropdownToggle does.
HoverCard
HoverCard is the star of the show! It powers the UI markup, styles, and some of the relevant event handlers for both tooltips and dropdowns. It contains no state and it doesn’t know if it’s open or closed. If it exists, it’s open. You close it by unmounting it.
It accepts an anchor element as a prop, which is the element that the floating HoverCard positions itself around. HoverCard also has multiple looks and feels, a.k.a. flavors. One flavor is called "tooltip" which has a black background and white text color. Another is called "dropdown", which is used by the the Select component, and it has a white background and a box-shadow.
HoverCard also takes in a myriad of props for customization, such as whether or not to show a triangular caret (TooltipToggle enables this prop), or what the position of the HoverCard is relative to the anchor ( TooltipToggle uses “top” while DropdownToggle uses “bottom”), and so on. HoverCard also listens for certain events (such as clicks) that happen outside of the HoverCard or key presses to the ESC key. When these events happen, HoverCard notifies the parent component via prop callbacks so that the parent can decide if it wants to close the HoverCard. One other responsibility of HoverCard is to detect whether it’s overflowing outside of the window and - if so - fix its positioning . (This functionality can be turned off via a prop).
Extracting all of the UI implementation code into HoverCard allows the higher level components like DropdownToggle and TooltipToggle to only focus on state management and interaction logic instead of re-implementing the nitty gritty, DOM positioning and styling code that is shared between all hover-y things in the UI.
This is just one example of separating UI details from interaction logic. Following this principle for all components and carefully evaluating where new pieces of state should live has really increased our ability to reuse code.
What about Flux?
Flux is great for storing application state that either doesn’t logically belong to any one specific component or state that should persist after an unmount. A common bit of advice is to never use this.state and to put everything in Flux stores -- however, this isn’t quite right. You should feel free to use this.state for component specific state that isn’t relevant after something unmounts. An example of such state is the isCurrentlyOpen state of DropdownToggle.
Flux is also quite verbose, which makes it inconvenient for data state, state that is persisted to the server. We currently use a global Backbone model cache for data fetching and saving but we’re also experimenting with a Relay-like system for REST apis. (Stay tuned for more on this topic).
For all other state, we have been able to gradually introduce Flux into our code base. It’s great because it doesn’t require a rewrite; you can use it where you want. It’s easy to unit test, easy to scale, and has provided some cool benefits like resolving circular dependencies in our core modules. It has also removed the need for hacky singleton components.
Reuse through React instead of CSS
The final scaling tip that we want to share in this post is this: ensure that React components are your primary unit of reuse. Every one of our React components has an associated CSS file. Some of our components don’t even have any JS interactions or functionality. They’re just bundles of markup and styles.
We’ve been staying away from Bootstrap-like global styles through class names. You can definitely still use Bootstrap, but wrapping the Bootstrap components in your own React components will save you time in the long run. For example, having an Icon React component that encapsulates that markup and accepts the icon name as a prop is better than having to remember exactly what markup and class names to use for icons, which in turn makes refactoring easier. It also makes it easier to add functionality to these components later on.
Although we do define a few global styles for elements such as anchors and headings, as well as many global SCSS variables, we don’t really define global css classes. Being careful to reuse UI mostly through React components has made us more productive as a team because the code is more consistent and predictable.
And that’s about it! These have been some of the guiding principles that helped us build a robust React architecture that has scaled with the size of our engineering team and the complexity of the application. Please feel free to comment with your thoughts and experiences with the ideas in this post.
Comments (0)
Sign in to post comments.