Resizing React Components
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).
- 6 items
- Can only fit 4
- (item) (item) (item) (+3)
React’s lifecycle methods and DOM event listenersI’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.
- 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”.
- 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 EventWe need to listen to the resize event on the window.
Note: The resize event occurs on the WINDOW and not the DOCUMENT. For example:
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
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
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 ContainerBecause of React’s built in ref attribute that it provides, finding the width of any DOM node is simple:
? 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,
Our render function would look something like this:
Our component now has the expected functionality, but there are some optimizations we should add.
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
call every time that occurs. Instead, we use debounce to space out the calls to resizing our container. Debounce only allows
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
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
in the componentDidMount method. However, we need to avoid calling
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
, it will run
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.