The other day, I ran into a really interesting situation in ReactJS. Normally, when I think about React, I think about the state of the component driving the virtual DOM (Document Object Model) and, subsequently, the physical DOM. But, I had situation in which I actually needed to update the state of the component based on the state of the rendered document. Solving this problem was not immediately obvious; and, it sent me into a few infinite loops and "Maximum call stack size exceeded" errors. But, eventually, I figured out how to leverage the setState() method as means to safely hook into the post-rendering phase of the component life-cycle.
In this particular case, I had a container that needed to render a list of items. The number of items that I could render wasn't hard coded but, instead, driven by the physical dimensions of the container. As the container shrank, I had to reduce the number of rendered items; and, as the container expanded, I had to increase the number of rendered items.
I knew that I had to keep the "visual count" in the component state so that I could .slice() my items within the render() function (which can't know anything about the physical DOM). But, I didn't know how to actually obtain and set the visual count. My first thought was to put this calculation inside of the componentDidUpdate() method so that I would recalculate it every time the virtual DOM was flushed to the physical DOM.
I probably could have jimmied the shouldComponentUpdate() method to help prevent the infinite loop; but, this felt like a hack on top of a hack. Ultimately, what I figured out was that the setState() method takes an optional second argument which is a callback that will be invoked after the state changes have been flushed to the DOM. This callback is very much like componentDidUpdate(); but, it only gets called once for a specific call to setState(). Now, instead of trying query the DOM after any arbitrary rendering, such as I was doing in componentDidUpdate(), I can query the DOM only after I know that it has been affected by relevant changes.
To see this in action, I've created a demo in which I have a horizontal container that renders a list of items. The container can be resized via CSS (full width vs. half width); or, it can be resized as the browser window is resized. In each case, I am providing a setState() callback that will allow me to hook into the post-render life-cycle where I can recalculate the number of elements that will fit in the container:
As you can see, I am providing the calculateVisibleCount() method as the setState() callback. This way, whenever I set state values that I know will affect the container dimensions, I know that I'll be able to query those container dimensions once they have been updated. When I run this page, the items fit nicely within the container:
In all cases, the state of the ReactJS component is what drives the rendering of the physical DOM. But, in some cases, that relationship becomes a bit bi-directional in that the state of the component must be, in part, defined by the state of the physical DOM. In such cases, it's awesome that the setState() method provides a hook into the post-rendering life-cycle of the virtual DOM reconciliation.
Want to use code from this post? Check out the license.
Some days, someone just perfectly solves your problem for you. I needed to figure out how to set a class on an element if its child exceeded its height (if it overflowed, on other words), and this was a perfectly clear explanation of how to do exactly what I needed. Thanks for taking the time to share it.