Resizing React Components
By: Hootsuite Engineering
Tags: JavaScript, Code, React, Co Op

While working at Hootsuite, I’ve had the pleasure of working in-depth with React to build our newest features for the dashboard. For example, I was tasked with building a component to show a horizontal list of items that adapted to the changing window size. Based on the window width, my component would show a dynamic amount of items and tell the user how many of those items were hidden if any. It’d be a fairly simple task if the amount of items was static, but but since the number of items is dynamic, I had to calculate how many to display each time the window size changed.

More specifically, the amount of items in the list should change depending on the size of the container. The last item in the list should show how many hidden items there are in the list if all the items didn’t fit (i.e +3).

For example:

6 items

Can only fit 4

(item) (item) (item) (+3)

React’s lifecycle methods and DOM event listeners

I’ll show you a solution on how to achieve this implementation with the help of React’s lifecycle methods and DOM event listeners.

Note: At the time the code was written, it was done using harmony jsx loader, which has a subset of ES6 features. The main ES6 features I use is arrow functions and the class notation for components.

Solution:

We need to check if the list of items fits the container when the window resizes.

If the container is big enough, we don’t truncate our list.

If it isn’t big enough, we need to figure out how many items didn’t fit and transform the list item into a “counter” that shows how many are now “hidden”.

How do we accomplish finding out if the items don’t all fit in the container?

We need to know when the window resizes so that we can determine if we should truncate more or less. We need to know the width of the container. We need to know the width of each item. If the combined width of items is bigger than the container, then we know we need to truncate our list.

1. Listening to the Window Resize Event

We need to listen to the resize event on the window.

Note: The resize event occurs on the WINDOW and not the DOCUMENT. For example:

window.addEventListener('resize', this._handleWindowResize)

instead of

document.addEventListener('resize', this._handleWindowResize)

So, when should we add this listener? It only makes sense to add this resize listener when the DOM is mounted and components have properly calculated their width and height. Therefore, we add this listener to componentDidMount() :

When should we remove this listener? We remove it when the component is unmounted, that way it’s easy to cleanup and we don’t need to worry about it later. The correct lifecycle method to place this in is componentWillUnmount .

We now have our event listeners setup to correctly respond to window resizing.

Now that we have our listeners setup to handle resize events, let’s put some functionality in them.

2. Width of the Container

Because of React’s built in ref attribute that it provides, finding the width of any DOM node is simple: React.findDOMNode(this._containerTarget).offsetWidth

What is this._containerTarget ? It is a simple reference to our div container that we created with the built in ref attribute.

Here is the code to make a ref:

Once we have the width, we setState and store the width:

Now that the function is created, I will bind it in the constructor like so:

3. Width of Each Individual Item

The second task, which is to find the width of each item, is trivial and dependent on your css. You can set a constant to the width in your component file and access it when necessary: var ITEM_SIZE = 60

4. Truncate the List of Items

For our last piece of work, we need to build the logic to truncate our list of items.

We need to divide the width of the container that we stored in the state by the width of each item. With that, we get the number of items we can fit in our container.

If that number is greater than the number of items, then we don’t do anything and leave the list as is. However, if it isn’t then we need to truncate the item list. For example, if the list can currrently fit 8 items and there are 10 items, then show 7 items and use the 8th item to show the remaining number of items (i.e +3):

Where the parameter, items, is an array of react elements that we want to display.

How will this method run? Since setState triggers a re-render, when our window is resized and we update our container width state, _truncateItems is run.

Our render function would look something like this:

Our component now has the expected functionality, but there are some optimizations we should add.

Optimizations

In React, the DOM is rendered every time the state is changed to update components and their children. As the resize event changes the state on every window resize, it would be overkill to let _handleWindowResize call every time that occurs. Instead, we use debounce to space out the calls to resizing our container. Debounce only allows _handleWindowResize to run after a specified time since the last call to that function. To implement this, I decided to use the underscore library’s debounce with a delay of 100ms. Doing this helps avoid re-rendering our components multiple times for one window resize.

So, where do we put our debounce code? Remember how we bound _handleWIndowResize in our constructor? this._handleWindowResize = _.debounce(this._handleWindowResize.bind(this), 100);

We add the debounce here, and NOT every time it’s called. Essentially, by putting it here we create one instance of this debounced function for this component and not multiple copies, which is what we want to avoid. This Stack Overflow article explains further.

Putting it to Test

If we put all of our code together, we’ll notice something weird happening: the items list doesn’t show up at all. Upon further inspection, the width of the container is actually zero. This is no surprise since I initialized the container width to be zero in my constructor, so the problem is that even though our component correctly responds to window resizing, it only runs when the window is resized and not on mounting of the component.

The naive solution would be to simply call _handleWindowResize in the componentDidMount method. However, we need to avoid calling _handleWindowResize anywhere we want, in the off chance the component has already been unmounted. If you recall earlier, we debounced our resizing to avoid additional rendering. However, it is possible that while the function is waiting to run again, we unmount our component and the function tries to set state on an unmounted component. When that occurs, the error in your browser console will look something like this:

“Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the undefined component.”

The solution I provide ensures set state is never called while this component is not mounted, and resizes the container when the component is mounted. I opted to manually keep track of when the component was mounted:

With this fairly trivial workaround, I use an instance variable to keep track of when the component is mounted. Since ref callbacks are called before componentDidMount , it will run _handleWindowResize on mount and we will be able to see our list.

Here is the complete code, and a working example in jsfiddle for your reference:

// js

// css

Voila! You now have a component that can dynamically update the number of items shown in a container for any time your browser window resizes.

Conclusion

At first glance, this project seemed straightforward and easy to implement. However, when optimizing the component’s rendering, it meant that I needed to look at all spots where I was needlessly calling set state or using set state illegally. It turns out that it could be abused in two spots, every time the window resize event fired and resizing the component after it was unmounted. Ignoring these two issues would’ve gone unnoticed in production but it means you aren’t cleaning up your component properly, which can lead to more problems down the road. After solving these two issues, the rest was smooth sailing.

About the Author

Daniel has been a co-op at Hootsuite for the past 8 months, making key contributions to the new Custom Approvals and brand new Bulk Composer Beta. He’s really enjoyed working with React to efficiently build and design the front end components for services to connect with. He looks forward to bringing the skills he’s gained from this co-op into his career in the future.