TL&DR To integrate React.js and Isotope you need to tell Isotope of changes to the DOM through Isotope's API (remove and addItems).
Overview
Pairing React.js with Isotope is not particularly difficult when dealing with static content. However, pairing the two with dynamic content does get a bit tricky. The problem with the combination is that Isotope, by default, expects a known set of DOM elements so that it can make computations internally. When the elements change, which will commonly happen in a dynamic environment, especially when doing ajax calls in React, the solution is not as simple as you would expect. Isotope needs to know of each change to the DOM, specifically each removal and addition of the items being tracked. React.js provides a suite of lifecycle hooks so we can tell Isotope when something has changed, but Isotope, by itself, will not just work if all you do is seed it with an initial set of DOM elements from a React render.
Isotope will sort and filter to your heart's content with initially loaded data, but if you want to load more data or reload data in the DOM, then Isotope is completely unaware of that new data (referring to React DOM updates and new DOM insertions/deletions). Also, frequently, the first set of props sent to child components is null or empty. The good news here is that you can still use the best of React with the best of Isotope.
I assume you are already familiar with Isotope, but if you aren't, Isotope is a library that incorporates filtering, sorting, and the arranging of elements in the DOM. Isotope does these things in a manner that is particularly visually appealing. You can see many examples, primarily the periodic table demo, at the main site at http://isotope.metafizzy.co/ David DeSandro and the Metafizzy team have done an absolutely incredible job with this library. I've dug into the internal Isotope code as well and the design is top notch.
The main periodic table demo I will be basing the React app on is http://codepen.io/desandro/pen/nFrte
The webapp I created for this post includes ES6/ES7 code and Flux so I chose to not create a Codepen or jsfiddle but to create a standalone/deployable app. You can find the source at https://github.com/engineersamuel/react-isotope
https://cloud.githubusercontent.com/assets/2019830/12128230/eae6b438-b3c9-11e5-8531-f0e7a339403e.png
The Code
While there is much more code to this app than I'll show here I want to really focus on the main class named IsotopeResponseRenderer.jsx. The rest of the code is primarily dealing with having some level of strucuture around React (react-router, Flux, webpack, ect...). I will detail a bit more on Flux later and why it's a very welcome addition to the mix.
First off we must define the class itself. This is standard ES6 React code with the addition of ES7 style decorators to add in Flux Alt. I'm quite keen on the Alt implementation of Flux primarily due to it's very concise expression and minimal boilerplate code. That said, Redux is probably the more favored one in the community right now and I've only heard extremely positive things about Redux. So whether you use Redux or Alt or another Flux impl, any Flux impl will provide you great advantages over not using Flux at all.
The @connectToStores enables the React component to listen to Alt Stores. And the Store the component is listening to is defined in the getStores() function. The getPropsFromStores() function simple binds the state of the Store to the props of the component. Note that the component can listen to any number of stores, in this example it just listens to one.
https://gist.github.com/engineersamuel/781910ac46bc84461f52
The constructor for a React component subclass is where you would define the component state and where we can define instance variables. At the time of this writing ES6 style React sub-classes appear to be the growing standard means of writing a React component. This is not universally true, but one of the current trends, and it's the means I've adopted for this example.
There are two primary instance variables set in the constructor, filterFns (copied from the Isotope example) and isoOptions. Neither actually have to be defined here. The definitions could have been placed above the class as module level variables or consts. That would be fine. If we wanted to have more control over the isoOptions we could have defined it in the component state, or in a new Flux Store. That would allow us to dynamically change the layout and sort data. For this example we'll keep it simple and have them as instance variables on the class.
The two notable parts of the isoOptions are:
- itemSelector: This tells Isotope the class for the DOM elements in which Isotope will control.
- masonry: columnWidth: There is an odd behavior without setting this. Without this the masonry layout will collapse into a vertical layout between ajax data loads. This is an open issue that I don't know the exact cause of yet, but the fix is setting the columnWidth to an example div with class of .element-item-sizer. It's common practice with Isotope to set an item sizer: http://isotope.metafizzy.co/options.html#element-sizing
https://gist.github.com/engineersamuel/0dce908f179e4cd5732c
When the component mounts via componentDidMount() we'll need to create the Isotope container for the first time. Then if there are actually children go ahead and tell Isotope to arrange those children based on the isoOptions defined earlier. If no children are given that's fine, the arrange will happen whenever the component updates with new data.
https://gist.github.com/engineersamuel/c4f25991da2a1ba70e5c
The createIsotopeContainer() instantiates a new Isotope reference with the DOM node of the div which is rendered in the render() function, and with a reference to the isoOptions which is set in the constructor. This reference is set as an instance variable on the component so that it can be accessed in any other React lifecycle function.
https://gist.github.com/engineersamuel/046db306ececfd1fee73
The render() function is pretty basic. There is a div which sets the className attribute to isotope for styling purposes and a ref attribute to isotopeContainer which you saw was used previously to lookup the div in createIsotopeContainer(). Then there is a element-item-sizer div which is ultimately set to the dimensions of the element-item class (See Home.less). This is so Isotope can know how to size each div. Finally there is the {this.props.children} which for this example is an array of React components of subclass Element.jsx.
https://gist.github.com/engineersamuel/74bff04eddc2e8cb3b1d
Next we have the shouldComponentUpdate(nextProps, nextState). This determines if the component should update, which would trigger a re-render and then the componentDidUpdate function. If the parent component or Flux components are properly handling updates then this component should not receive identical data, thus no re-render. However if the parent components are re-rendering on every change, then it's best to test that there considering that there is additional logic in play in the componentDidUpdate. From a defensive standpoint, and for this particular use case, where the component may receive a large array of children, we'd not want to trigger an update unless necessary. I tend to prefer this defensive style or an even more targeted style of testing equality. That said, sometimes the cost of testing equality is greater than simply re-rendering the DOM every time. In a performance critical app you would want to test this and evaluate if this may be true.
https://gist.github.com/engineersamuel/8e3f518e2d621a05da36
The component must deal with what happens when new props are received. The new props will be received either through the Home component re-rendering and passing new child Elements, or through the FilterSortStore alerting of a new filter or sort state. This is handled through componentWillReceiveProps(nextProps). The component will receive new props via the flux listeners when any new sort or filter is set. In the parent component not shown here (Home.jsx) if a new sort or filter is selected, a FilterSortActions function is invoked and the filter or sort set in the FilterSortStore state. Since the IsotopeResponseRenderer component listens to the FilterSortStore then any state changed in the FilterSortStore will be received as a nextProps and trigger the componentWillReceiveProps(nextProps) function where nextProps is the FilterSortStore state. It's here then that we must deal with telling Isotope how to handle the filtering and sorting.
The logic flows as thus. If there is a filter or sort on the nextProps and that filter or sort does not equal the previous filter or sort, then invoke the Isotope arrange function with that filter or sort.
https://gist.github.com/engineersamuel/a17ead40675f3242452c
The componentDidUpdate(prevProps) is where we get to the real meat of the React and Isotope integration. Without the logic contained herein, Isotope would not work right.
The main concept, encapsulated in this function, is to inform Isotope what new DOM elements have come into the view, and what old DOM elements have left the view, that simple.
Since componentDidUpdate(prevProps) is invoked after the DOM is rendered (but not the first render) then we know whatever is in this.props is the latest data and whatever is in prevProps is what needs to be culled. Given this it's easy to get a list of new keys and old keys. From this point we tell Isotope, internally, to remove the elements that are not longer in the active DOM and to add, internally, any new elements that are new in the active DOM. When I say active DOM I mean whatever is the current representation of the DOM that React has rendered/re-rendered.
Isotope maintains an internally array of DOM references called items. When React updates the DOM we have to tell Isotope of the new references and tell it to remove the old reference, this is exactly what is happening below, and it's the main consideration when integrating React and Isotope.
https://gist.github.com/engineersamuel/27ba0e898d263708fb8d
Flux and why it helps
The power of using Flux is that you have the data layer separated from the the rest of your View layer. The source of truth for the data then resides in the Flux Stores and any listening component simply reacts to changes. This enforces a unidirectional data flow and leads to much less headache when your application increases in scope, complexity, and interaction.
This is a vastly superior way to handle data flow than by embedding that flow into the Components themselves where you'll have to manage more complex parent child interactions.
With this React and Isotope example, instead of passing attributes for sort and filter to the IsotopeResponseRenderer in Home.jsx, we tell the IsotopeResponseRenderer and Home to listen to the FilterSortStore. Then if we want to change the filter or sort, we invoke one of the actions that updates the state in the store. The alternative would be to maintain the filter and sort state in the component itself, which is not as flexible.
By defining the state above the Components in a Store any individual Component can rely on that state by connecting to the store.
You can see below how the filter and sort buttons are wired to the Flux actions. There really isn't much to it.
Using Flux (a flux impl) let's us avoid more complicated means of managing state in the Component state itself and also propagates that state down to child components as we dictate.
https://gist.github.com/engineersamuel/d4b3494ead3598fb6e08
Concluding Thoughts
Isotope provides a very powerful means of arranging elements in the DOM. React provides a very powerful and concise means of rendering that DOM. Combining the two works extremely and you can produce very appealing interfaces. If this sort of thing interests you definitely check out the source code at https://github.com/engineersamuel/react-isotope. This project also provides a complete example using Webpack which may be a helpful reference as well.
Last updated: February 23, 2024