After making our journey from Flux to Redux, we were happy with how clean and simple our state management had become so we kept ticking along.
Redux containers fetch data from the store by connecting to it and transforming the state into props to be passed to the component. For example:
mapStateToPropsmethods, resulting in:
The Pain – Breaking Production \o/
After adding the connected component from the above snippet (
SearchBarContainer) to our soon-to-be-released desktop pages, we realised that the transition from search results to property page was broken (only on desktop so it wasn’t affecting real users phew).
Something was being executed with unexpected input mid-transition, blowing up and blocking the page from being rendered. The problem, aside from handling this input incorrectly, was that this piece of code was supposed to only be reached when the search results page is rendered. So why?? We had suspicions but wanted to prove it.
Background – The Virtual DOM
In order to prevent expensive re-rendering, ReactJS maintains a virtual copy of the DOM. Each time a change is triggered, a new copy of the virtual DOM is created. The two versions of the DOM are compared to determine whether the real DOM needs to be updated. This is done by traversing the tree and determining whether the props or state of a given component have changed.
Proving the Theory
Time to finally getting around to reading the Deep Dive Into Rect Perf Debugging post that had been waiting on our lists for weeks and break out the ReactJS performance tools.
Like the ReactJS testing utils, there is an add-on available which bundles up the ReactJS performance tools. These tools can be used for measuring client side events by exposing them to the browser console in the application client entry point.
window.ReactPerf = require('react-addons-perf’);
To measure the performance, start the tracking (
ReactPerf.start()), perform the action (in this case click on a listing on the search results page) and then stop the tracking (
ReactPerf.stop()). The results, including wasted render events, can then be printed to the console.
Printing the wasted render events for our page transition (
ReactPerf.printWasted()) revealed that our entire tree of search bar components was being re-rendered before moving on to the next page. While this may not be the most expensive of re-renders, it is unexpected.
The Benchling engineering blog post provides a code snippet for a mix-in (
WhyDidYouUpdateMixin) which they use to identify why a component has updated. By taking the mix-in (and turning it into a higher-order component because mix-ins are dead), we were able to wrap all of the suspect components and inspect what was happening.
It works by hooking into the
componentDidUpdate lifecycle of components. This is executed after a component has been re-rendered and receives the state and props it was previously rendered with. By performing a deep comparison of the data, it can determine whether the component really needed to update and log the results to the console.
Digging through the objects in the console, it was clear that none of the data had actually changed. So what had changed? The object reference had!
An action was being fired as a part of the page transition which updated the Redux store state, causing all of the containers to re-evaluate all before the first page had been torn down.
Despite the filters state remaining unchanged, a new instance of the view model was being created every time thanks to this:
Like other libraries popping up in the ReactJS and Redux worlds, we saw Reselect make an appearance but couldn’t see a place for it in our application.
Reselect provides a way of “selecting” data from Redux stores and computing derived data, allowing us to store only the minimum state in our stores rather than having to maintain different views of the data required by different ares of the application.
The most important feature of Reselect selectors is that they are memoized. This means that a selector will not be recalculated unless the input for it has changed, rather just returning its previous result. Exactly what we needed!
All that was needed to solve our re-rendering problem was to add the following selector:
And use it to extract the data required for the
As you can see, selectors are composable and can be used as input to other selectors. For example,
getFiltersChannel in the above snippet.
Reselect isn’t really magic. Under the covers it performs a very simple comparison of the arguments. While the equality check is configurable, for many cases, the default is enough to prevent excessive re-rendering.