The repo associated with this article can be found here

Recently at work, I developed a React component with a fixed height, but whose content could have a greater height than the parent div. The div holds a variable number of inputs, whose count is determined by the user, who can add an input by clicking a button. I applied an overflow: scroll css property so that the user could scroll down and use the input they added. Here’s a simplification of the component:

But I also wanted the input container to scroll to the bottom every time an input was added so that the input would be in view of the user. This can be accomplished with a combination of the useRef and useEffect hooks.

Start by instantiating two refs. Let’s call the first prevLengthRef , and we’ll set it to the initial length of our inputs array. Next, we’ll instantiate another ref, called containerRef , and we’ll initiate it with null . Finally, we’ll create another variable called prevLength and set it to prevLengthRef.current . This seems redundant for now, but it’ll make sense in a moment.

function View() {

const [inputs, setInputs] = useState([])

const prevLengthRef = useRef(inputs.length)

const containerRef = useRef(null)

const prevLength = prevLengthRef.current

...

}

We’ll make use of useEffect next to compare the value of prevLengthRef and inputs.length to determine if we should perform an autoscroll after a render. We want this comparison to be made any time that the length of the inputs array changes. So let’s put that value in our deps array.

function View() {

...

useEffect(() => {

//some logic to perform autoscroll

}, [inputs.length])

...

}

We’re declaring we’d like to fire the callback, and potentially the autoscroll, anytime the .length property of inputs changes. In reality, we really only want the autoscroll to happen when the length increases. We can’t do this kind of check in the dep array, so we’ll put this logic inside the callback. This is where we’ll make use of that seemingly redundant variable declared above — prevLength . This variable holds reference to the value of the previous render’s inputs.length value. Inside the useEffect callback, we compare that value to the current value of inputs.length , and if it’s smaller, we know we’ve added an input element, and we perform an autoscroll. At the end of the callback, we set our ref’s .current property to the value of inputs.length , in anticipation of the next render and comparison.

function View() {

...

const prevLength = prevLengthRef.current

useEffect(() => {

if (prevLength > length) {

//some logic to perform autoscroll

}

prevLengthRef.current = length

}, [inputs.length])

...

}

Most of the pieces are there. Now we need to write the logic to actually mutate our DOM and scroll to the bottom of our div. The first thing to do is pass the containerRef as the ref prop in our input container.

function View() {

...

return (

<div className="view-container">

<div ref={containerRef} className="inputs-container">

...

}

Inside of our useEffect we’ll perform our operation on the ref . It’s pretty simple. The .current property of our containerRef points to the div node, and as such, includes .scrollHeight and .scrollTop properties. scrollTop is a measurement of the distance from the element’s top to its topmost visible content. scrollHeight is a value representing the elements height including the pixels not in view. If we set scrollTop to scrollHeight , it creates the effect of an autoscroll ✨.

function View() {

...

const prevLength = prevLengthRef.current

useEffect(() => {

if (prevLength > length) {

const container = containerRef.current

container.scrollTop = container.scrollHeight

}

prevLengthRef.current = length

}, [inputs.length])

...

}