Best practices for building large React applications
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 ownership
Bump 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.
Check out React Tips and Best Practices if you’re looking for even more!
33 Comments
May 8, 2015 at 2:58 pm
Excellent work.
May 9, 2015 at 4:20 am
Congrats and great advice!
I’ve had pretty good success reducing the verbosity of Flux by using https://www.js-data.io/ with React. Here’s a project of mine as an example: https://github.com/jmdobry/RequelPro/tree/master/src/RequelPro
(Disclaimer: I am the author js-data, and I’m totally interested in any feedback on how well it plays with React.)
May 11, 2015 at 5:08 pm
JS Data looks really powerful! I especially like that you can plug in different adapters and hook into the data lifycycle. I’ll definitely try this out with React next chance i get, thanks for putting this on our radar!
May 9, 2015 at 1:53 pm
Great article!
May 9, 2015 at 2:23 pm
I get nervous about creating so many components because I feel like I have to pass props so much. Better to have fewer components so you don’t need to switch files and pass props all the time?
May 11, 2015 at 5:03 pm
Creating many reusable components makes the code conceptually simpler, but it’s a bit harder to design initially. In the simple vs. easy debate, simple usually wins 🙂 I believe that extensive use of props is healthy. It enables compositions and allows you to make the most out of React.
May 9, 2015 at 2:57 pm
"Reuse through React instead of CSS", how do you maintain a consistent look-and-feel across components? So if I want the background or font-size consistent across multiple components, isn’t refactoring easier when they are global? If I decide to change something, won’t I have to touch all CSS across all components to match the look and feel?
May 11, 2015 at 4:38 pm
We actually use global SCSS variables. Duplication of css should still be avoided, so if we can’t achieve look-and-feel consistency through component reuse then we try to do it through SCSS variables.
May 16, 2015 at 5:44 pm
Does this strategy work for React Native too?
June 5, 2015 at 4:28 am
Ran into this recently, how do you access SCSS variables within the component? Say a borderBottom: ‘1px solid $GREY’?
June 5, 2015 at 9:30 pm
We include all .scss files into one main .scss file. The only .scss file that our build tool knows about is the main one. That way, whatever variables you declare will be available in component specific .scss files. I don’t think this is possible if your build process builds every .scss file separately though.
May 9, 2015 at 6:14 pm
How do you include the css files with the component at render-time?
May 11, 2015 at 5:20 pm
We actually compile and concatenate all of the component specific css files into one, and that’s included on page load. They don’t really interfere with each other because every css file namespaces its styles under a top level class name. Here’s a quick example of the SCSS file for the Button component https://cssdeck.com/labs/2ycvufgy.
May 10, 2015 at 3:55 am
Excellent Advice.
"Every one of our React components has an associated CSS file." Can you comment more on how to applying this best practice with Bootstrap Components, maybe with an example?
Thanks.
May 11, 2015 at 6:12 pm
Sure! I can do an example for to Bootstrap "Badge" component (https://getbootstrap.com/components/#badges). It’s a simple example but I think it’s a good demonstration of the idea.
Notice that the Bootstrap Badge component is self-collapsing, meaning that if it doesn’t have any content then the badge doesn’t show at all. However this functionality doesn’t work in IE8 because it depends on the :empty selector. Let’s wrap it in a React component and make it IE8 compatible!
We will want to:
1. Create a React JSX file that just renders a Bootstrap badge
2. Create an associated SCSS file that overrides Bootstrap defaults for a custom look & feel. In this example we will just change the font size of the badge to be HUGE!
3. Add some if-else logic into the render method so that nothing gets rendered if there are no children. This way we implement the self-collapsing functionality without depending on :empty.
Here’s the result: https://jsfiddle.net/69z2wepo/8025/
Note that the style rules in our component specific SCSS file are namespaced under the class name of the component (.MyBadge). Also, as the user of the MyBadge component, I don’t need to know anything about Bootstrap. That implementation detail is completely hidden from me!
May 11, 2015 at 10:16 am
Great demonstration of separation of concerns at a component level.
May 12, 2015 at 2:44 am
Thanks for sharing these tips. First anti-pattern I saw with React code was the component coupling you described. The second was not following the single reuse principle. Nailed that one too. But I’d advice anyone reading to avoid stuffing CSS into components unless you’re using a single per-app strategy like BEM (or SMCSS, like Bootstrap) lest you quit before any kind of site redesign or rebranding effort. Good luck!
May 12, 2015 at 7:01 pm
Hi do you guys use className={} for adding those classes that are related to that component? Or is it inline CSS style={}?
May 12, 2015 at 8:21 pm
We use className and stylesheets extensively. Inline styles sparingly but this does look pretty interesting: https://speakerdeck.com/vjeux/react-css-in-js
May 26, 2015 at 8:39 pm
How are you packaging all your React logic? Are you using a tool like Browserify and a task runner like Gulp.js? And how are you minifying/compiling your JS files as you transition to a composable paradigm from a Backbone, and possibly, jQuery design?
May 27, 2015 at 6:55 pm
Yep we use both Gulp and Browserify. We used to use the Rails asset pipeline but once we removed Rails from our stack, we started building assets with Gulp.
June 18, 2015 at 3:51 am
Can you please tell me what you do when you have a lot of components and don’t want a large single js file?
June 16, 2015 at 9:27 pm
Can you create a sample code for "Separating UI details from interaction logic"? The word-explanation is tripping me up. Thanks.
June 29, 2015 at 9:16 am
+1. I would love to see examples of the HoverCard compositions.
June 18, 2015 at 4:18 am
First, thanks for this awesome article. I have a question: Imagine that you have a component that represents a table, so you end up having several tablerows component. If you want to reuse this table component, depending on the situation the row will look different (sometimes the markup will include an image for example). How do you deal in that situation? When the inner components are different depending on the use of the parent component?
June 19, 2015 at 11:00 pm
We handle component customization in 2 ways. One technique we use is to declare various "flavors" that the component could have. This is generally used for components that have very similar functionality, but have different look-and-feels based on the context. The other way we customize is simply through specific props. Your example with the Table component can be solved in either way. Are there conceptual differences between a table with images and a table with none? If a table has images in your application, is it likely to have some other property as well? Or a different look and feel? In this case it could make sense to make a separate flavor of the Table component that shows images and possibly some other stuff. On the other hand, the there could be no higher level semantic differences that you establish for tables that have images vs images that don’t. In that case, I would just make a simple prop called "hasImages" or an array "imageUrls", or something.
August 11, 2015 at 7:55 pm
In the case of your reusable table, what I’d do is pass the table a "data" prop with the table data, and a "columns" prop which has the column definitions. The "columns" prop is an array containing one object for each column, and these objects have usually four keys: a "key" which is used to get the column value from the row of data, a "title" which is used to display the column header, a "width" if you’d like to set the width of the column, and an optional "template" which is a function taking a data row item and returning a component or string to render in that column.
Then the table component and each row component inside it will all include a loop through the column definitions in their render functions. The table component loops through to render the title row. Then each row component is passed its row data object, along with a reference to the same column definitions. Each row loops through the column definitions, and uses the key to look up the value in its data object if no template is provided, or runs the template passing in the data object if the template exists.
This way, your table can be configured to have a variable number of columns, and as many images or template columns as you need. I’ve used this technique and it is a great way to avoid duplicating column definitions.
July 13, 2015 at 6:10 pm
Hey Alex, great article! Is there any chance you could share the source at least for the example components you mention in this article. It would be really helpful just to see the code behind these explanations. Thank you!
August 13, 2015 at 5:03 pm
Hey Alex, any word on code examples? This was an awesome article, there isn’t much information out there regarding scaling react applications with the approach your describing. It would be much appreciated if you could provide a small of example of your Select component. Thanks!
December 2, 2015 at 3:06 pm
React.js Tips & Best Practices – we are putting together collections of best practices and tips to help you solve problems and speed up development. Today, Toptal network members are pleased to add a new set of React.js best practices and tips. This is a community-driven project, so please read them, comment on them, or even contribute your own. https://www.toptal.com/react/tips-and-practices/#remote-developer-job
December 3, 2015 at 7:36 pm
I just ran across this articles and I also think that code samples would be incredibly helpful, particularly in the section for "Separating UI details from interaction logic"
March 2, 2016 at 7:24 am
Great post. I would love to see some code to go along with the examples in "Separating UI details from interaction logic".
March 6, 2016 at 9:00 am
I would call it "our experience", not "best practices" since you decided not to dive deeper into the unidirectional dataflow and instead declare it "too verbose" (which is somewhat right) and "inconvenient for data state" (which is not right). I would suggest researching Flux implementation alternatives, for example Redux. Also, remember that Flux action dispatching pattern fits very well with task-oriented UI, which in turn fits very well with CQRS backend. For CRUD backend I would partially agree that Flux makes it more complex but for this reason I wouldn’t avoid Flux but try getting rid of CRUD instead.