We recently picked up a Rails application that was a few features away from completion. This application allows our Genome Team to classify multiple artworks based on visual and art historical characteristics. These characteristics, or "genes", can be added, removed, and changed for any of the artworks on the panel.
Our genomers are masters of efficiency, and over the years we have worked closely with them to tailor a dynamic interface to their specific workflow.
When we started working on the app, the back-end was organized, modular, and interfaced seamlessly with the Artsy API, but there were still a few front-end features we needed to implement before it could be used by the Genome Team. The app did not use a front-end framework, and as our features scaled it was difficult to keep track of UI state with pure CoffeeScript and global event listeners. Eventually, we decided to stop trying to patch our leaky roof and build a new one.
Choosing a Suitable Framework
We decided to introduce a front-end framework to make it easier to add new features, and spent a day researching different options. Our requirements were:
- A robust view layer that could work on top of our already-solid Rails back-end,
- A framework performant enough for an interaction-heavy single-page app with hundreds of editable fields autosaving on change,
- A streamlined framework that favors freedom over unnecessary structure.
We chose React, Facebook's view layer framework, because it provides much-needed structure and support for components in a single page app without too much boilerplate.
Our plan was to eventually replace all of the existing
*.haml.erb templates and global CoffeeScript mixins with
discrete React components. We used the react-rails gem, which easily
integrates React components with Rails views.
In line with the React tutorial, we first broke up our UI into functional and visual components. For each component we found the relevant HAML template, converted it into jsx and React components using dummy data, and eventually updated it to accept the correct state from our top-level component which did all of the dynamic fetching and saving. Then we deleted the associated HAML and CoffeeScript code.
Thinking the React Way
At this point we have replaced the majority of the app's front-end with React components. We love React because it encourages you to follow certain ideological conventions, but it does not force you into a structure that may not exactly align with your goals.
In React, having a single source of truth is ideal. Gone are liberally distributed global event listeners that can conflict and cause pages to get bogged down with transition logic. State is held at the topmost level in React and when state changes, React automatically re-renders only the affected components.
For example, we hold a hash
artworks in the highest-level state of the page:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
We also store a method at this level to update the
artworks state when there is a change:
1 2 3 4 5 6 7 8 9
That method is passed to child components, and when there is an update to an
artwork, such as when it becomes
selected, we invoke it to update all affected components:
1 2 3 4 5 6 7 8
React lets us define our components and interactions in a declarative style instead of stringing together possible transitions triggered by events. Before converting this app to React, we had many bugs around form submission and saving genome progress. However, by modeling state instead of UI transitions, we can easily track changes and save progress incrementally in the background without requiring a page refresh from the user.
From CoffeeScript to React: Selecting Artworks
In this app, genomers are able to 'select' artworks on the panel for the purposes of saving and conducting batch actions. In our initial implementation, clicking the 'select all' button would individually click each artwork and used global event listeners to change UI state:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
With React, we store whether or not an artwork is selected as part of our state, and the appearance of elements
results from this variable. We use class sets
to dynamically alter styles such as button color. When the
selected state changes, React re-renders all
components that depend on that variable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
React's Virtual DOM
React keeps track of a Virtual DOM created by components you define. This can lead to issues, especially when trying to integrate React with jQuery plugins. For example, our modals kept showing up within other components until we explicitly rendered them on the outermost level. We also had issues trying to use an existing drag/drop plugin with the way we set up our state, and ended up building one from scratch.
React also crashes when the Virtual DOM becomes out-of-sync with the page DOM. We unearthed a mysterious bug in
which the browser was automatically inserting a
tbody tag when it saw a table rendered without one... causing
React (and therefore our entire app) to crash. In order to rectify this, we had to explicitly include these
normally optional tags:
1 2 3 4 5 6 7 8 9 10
Working with the React Lifecycle
In this case, we found it more straightforward to go with the jQuery solution.
React == Refactor
When we started out converting the app to React, it was hard to know whether or not an element should be its own component or if it could exist within another one. Often when we add new features, we have to refactor to make sure that we are reusing components and maintaining a single source of truth.
For example, we originally had one component to hold metadata on an artwork, such as artist, title, and date:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
When we implemented a new 'minimized' view for artworks, we also showed the title and artist, and so we broke these bits of information into separate components:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
And updated our parent to reuse the new child components:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
Our main challenge with specs had to do with RSpec not waiting long enough for components (such as autocomplete results) to appear, perhaps due to React's Virtual DOM. We spent many sad hours debugging spurious tests, and even included a few dreaded 'sleep' commands. Eventually, we integrated the rspec-retry gem to retry spurious tests during CI.
Converting our app to use a React-based front-end went surprisingly smoothly. We were able to incrementally change certain templates to React components, which made it easy to test as we went along. Additionally, our development time in adding new features since then has decreased dramatically. It is much easier to add new components or edit existing ones when there is a single source of truth and you don't have to search through global event listeners.
Choosing a front-end framework is non-trivial but incredibly important, and we are glad we found React. Because it does not require much overhead and it is possible to only use it on a portion of a page, React can be integrated into small or large projects. Although we deliberated for a long time over whether or not to use a framework, we never regretted moving to React and investing in the future of the app.