Dealing with scroll position when you insert content is usually a difficult problem to solve. We'll see how to use React life cycle methods to solve it elegantly.

Insertion at the bottom

The first example is to maintain the scroll position at the bottom when an element is inserted at the bottom. A common use case is a chat application.

In order to scroll at the bottom, we can do that on componentDidUpdate. It happens every time the element is re-rendered.

componentDidUpdate: function() {
  var node = this.getDOMNode();
  node.scrollTop = node.scrollHeight;
},

But this is going to always scroll to the bottom, which can be very annoying if you want to read what was above. Instead you want to scroll only if the user was already at the bottom. To do that, we can check the scroll position before the component has updated with componentWillUpdate and scroll if necessary at componentDidUpdate

componentWillUpdate: function() {
  var node = this.getDOMNode();
  this.shouldScrollBottom = node.scrollTop + node.offsetHeight === node.scrollHeight;
},
 
componentDidUpdate: function() {
  if (this.shouldScrollBottom) {
    var node = this.getDOMNode();
    node.scrollTop = node.scrollHeight
  }
},

Note: we use this.shouldScrollBottom = ...; and not this.setState({shouldScrollBottom: ...}); because we don't want to trigger another render. We just need to manage that value between the two events.

Insertion at the top

The other use case is adding elements at the top of the page but doing so without screwing up the current scroll position of the user. An example is a log view where you can scroll to the top to read historical context.

This is using a similar technique. On componentWillUpdate we store the scroll position and on componentDidUpdate we scroll to the added delta.

componentWillUpdate: function() {
  var node = this.getDOMNode();
  this.scrollHeight = node.scrollHeight;
  this.scrollTop = node.scrollTop;
},
 
componentDidUpdate: function() {
  var node = this.getDOMNode();
  node.scrollTop = this.scrollTop + (node.scrollHeight - this.scrollHeight);
},

Conclusion

React has not been designed to handle scroll position natively. However, it provides escape hatches from the declarative paradigm in order to be able to implement them.

If you liked this article, you might be interested in my Twitter feed as well.
 
  • James I.

    I especially like your second example. It is really common for sites with real-time streams to insert content above the fold, and it almost never makes sense to shift down the content that the user is currently reading. In the longer-term I think CSS and the browser should implement this behavior for us. I brought it up awhile back on the CSS WG list but it didn't really catch on: http://lists.w3.org/Archives/Public/www-style/2012Sep/0089.html.

    In the meantime, handling this with React is really nice since all of the componentWillUpdate calls can run before React modifies the DOM, nicely batching together all layout reads before dirtying the layout. Yet another example of how React avoids layout thrash in a practical manner.

  • mhils

    Thanks for the great snippet!
    Small correction: It should be node.clientHeight instead of node.offsetHeight, as the latter one includes border - scrollHeight doesn't.

  • metabrew

    Great examples, thanks!

    Is there a nice way to do something like occlusion culling, so only the dom nodes that would be visible are rendered?

    With fast infinite streams the amount of dom nodes would grow quickly, and would presumably get a bit slow without some sort of management.

 

Related Posts

  • September 24, 2011 Javascript: Cyclic Object Detection (4)
    URLON.stringify() suffer from a problem, when passed an object that contains a cycle, it will never stop. This article shows 3 techniques in order to detect if an object is cyclical. Edit the object: Mark In order to detect a cycle in an object, the method we learn at school is to mark each v...
  • September 14, 2011 CSS – One Line Justify (13)
    I came across a CSS problem, text-align: justify does not work with only one line. Justify behavior The reason is because it has been designed with paragraphs in mind. It justifies all the lines but the last one. Normal Justify Lorem ipsum dolor sit amet, consectetur adipisicing elit,...
  • November 4, 2013 Bitwise Truthiness (0)
    In this blog post, I explore another form of truthiness in Javascript. What happens if you use a bitwise operator on a value like 0|value or ~~value. Context We recently turned on the JSHint bitwise rule by default and the following code was caught. var isValid = false; for (var i = 0...
  • August 25, 2009 Test – Are you a Javascript Guru? (0)
    Javascript is a very flexible language, I made a compilation of some edge cases that you may have encountered while programming. The main goal is to point out some interesting specific behaviors. Concatenation 1] var result = [10] + 1; [10, 1] "101" 11 Explanation: The Array doesn't ...
  • June 8, 2012 CSS – Absolute position taking into account padding (4)
    When looking at the code of Lightbox.com I remarked that they are not using top and left in order to position their images but margin-top and margin-left. I've been wondering why for some time and finally found the reason. It is a way to position absolutely elements in a container and preservi...