Using Isotope with ReactJS

Isotope is a great layout alternative to Masonry, and recently it has been my preferred choice when going for a Masonry-like grid layout. But when I tried to add it in to a React application I’ve been working on, I encountered some issues with initialization. And things got reaaaally wonky when the grid items were dynamic and changed too. So here’s how I solved it.

I had a component for my product search results that rendered the product results that matched a user query then allowed users to filter them via a search box…as well as re-run the search again with different parameters. Each product result was displayed via the Product component, which essentially renders the product image, detailed specs, and then allows for selection/ordering.

The product data is retrieved from the ProductStore, which has data stored from a previous ajax request to an API server, which returns a JSON response with all the matching products. The product data retrieved is in something like the following format:

[
	{
		item_num: 0,
		name: "Awesome product",
		specs: [...] // an array of specifications
	},
	{
		item_num: 1,
		name: "Another product",
		specs: [...] // an array of specifications
	}
]

However, since we’re filtering the product data and also re-running the search with different search queries, the product data that needs to be displayed is dynamic. So, what we need is for the Isotope grid to adjust its layout accordingly every time the list of visible products changes due to user interaction.

The obvious solution would be to initialize Isotope in componentDidMount, then destroy it in componentWillUnmount. But when the product data changes….everything breaks. So how can we fix this so that even newly updated product results will be in an Isotope grid?

The solution is to remove the existing Isotope instance in componentWillUpdate, then reinitialize it in componentDidUpdate once the component has the new data. This is, of course, in addition to initializing Isotope during componentDidMount and destroying it in componentWillUnmount.

To initialize Isotope on a container element (in my case, .products):

$('.products').isotope({
	itemSelector: '.product',
    	layoutMode: 'masonry'
});

And to destroy/tear it back down:

$('.products').isotope('destroy');

Here’s a simplified version of what the final ProductSearch component looked like (note, JSX is commented out due to limitations of the Syntax highlighter):

var React = require('react');
var _ = require('lodash');
var Product = require('./Product');
var ProductFilters = require('./ProductFilters');
var ProductStore = require('./ProductStore');

var ProductSearch = React.createClass({
	getInitialState: function(){
		return {
			products: [],
			searchString: ""
		}
	},
	componentWillMount: function(){
		ProductStore.addChangeListener(this.handleReceiveData);
		this.setState({
			products: ProductStore.getProducts()
		})
	},
	handleReceiveData: function(){
		this.setState({
			products: ProductStore.getProducts()
		})
		console.log('data received from product store');
	},
	filterItems: function(event){
		this.setState({
			searchString: event.target.value
		});

	},
	render: function(){

		var vehicle = this.state.vehicle;

		// hey, if we have products stored in the state, display them
		if(this.state.products && this.state.products.length){
			var searchString = this.state.searchString.toLowerCase();
			var products = this.state.products.map(function(product){

				// combining the values of each product (e.g. item number, product description, technical details, color, etc into one string for search
				var productString = _.values(product).join("|").toLowerCase();

				// if there is no search string or if the search matches, then show the product
				if( !searchString || productString.match(searchString) ){
					// unfortunately, JSX doesn't play so well with the syntax highlighter. But this is JSX here
					return (
						//<Product product={product} key={product.model.toLowerCase() + product.finish}></Product>
					)
				}else{
					// otherwise, don't show the product in the results
					return "";
				}
			});
	
		}else{
			// no products were found
			var products = (
				//JSX <div>Sorry, nothing here.</div>
			);
		}

		return (
			/* JSX
			<div>
				<section id="products">
					<div>{products}</div>
				</section>
			</div>
			*/
		)
	},
	isDeviceMobile: function(){
		if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
		 return true;
		}
	},
	initIsotope: function(){
		$('.products').isotope({
	        	itemSelector: '.product',
	        	layoutMode: 'masonry'
	    	});
	},
	removeIsotope: function(){
		$('.products').isotope('destroy');
	},
	componentDidMount: function(){
		// initialize isotope only if there are products, and if the device isn't Mobile
		if(this.state.products && this.state.products.length > 0 && !this.isDeviceMobile()){
			this.initIsotope();
		}
	},
	componentWillUpdate: function(){
		this.removeIsotope();
	},
	componentDidUpdate: function(){
		if(this.state.products && this.state.products.length > 0 && !this.isDeviceMobile()){
	   		this.initIsotope();
	    }
	},
	componentWillUnmount: function(){
		this.removeIsotope();
		ProductStore.removeChangeListener(this.handleReceiveData);
	}
});

module.exports = ProductSearch;

Enjoy, and happy coding! 🙂