Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React: scrollIntoView only works inside of setTimeout

My application consists of a basic input where the user types a message. The message is then appended to the bottom of all of the other messages, much like a chat. When I add a new chat message to the array of messages I also want to scroll down to that message.

Each html element has a dynamically created ref based on its index in the loop which prints them out. The code that adds a new message attempts to scroll to the latest message after it has been added.

This code only works if it is placed within a setTimeout function. I cannot understand why this should be.

Code which creates the comments from their array

comments = this.state.item.notes.map((comment, i) => (
      <div key={i} ref={i}>
          <div className="comment-text">{comment.text}</div>
      </div>
    ));

Button which adds a new comment

<input type="text" value={this.state.textInput} onChange={this.commentChange} />
<div className="submit-button" onClick={() => this.addComment()}>Submit</div>

Add Comment function

addComment = () => {
    const value = this.state.textInput;
    const comment = {
      text: value,
      ts: new Date(),
      user: 'Test User',
    };
    const newComments = [...this.state.item.notes, comment];
    const newState = { ...this.state.item, notes: newComments };
    this.setState({ item: newState });
    this.setState({ textInput: '' });
    setTimeout(() => {
      this.scrollToLatest();
    }, 100);
  }

  scrollToLatest = () => {
    const commentIndex = this.state.xrayModalData.notes.length - 1;
    this.refs[commentIndex].scrollIntoView({ block: 'end', behavior: 'smooth' });
  };

If I do not put the call to scrollToLatest() inside of a setTimeout, it does not work. It doesn't generate errors, it simply does nothing. My thought was that it was trying to run before the state was set fully, but I've tried adding a callback to the setState function to run it, and it also does not work.

like image 690
mls3590712 Avatar asked Aug 21 '18 20:08

mls3590712


1 Answers

Adding a new comment and ref will require another render in the component update lifecycle, and you're attempting to access the ref before it has been rendered (which the setTimeout resolved, kind of). You should endeavor to use the React component lifecycle methods. Try calling your scrollToLatest inside the lifecycle method componentDidUpdate, which is called after the render has been executed.

And while you're certainly correct that setting state is an asynchronous process, the updating lifecycle methods (for example, shouldComponentUpdate, render, and componentDidUpdate) are not initiated until after a state update, and your setState callback may be called before the component is actually updated by render. The React docs can provide some additional clarification on the component lifecycles.

Finally, so that your scroll method is not called on every update (just on the updates that matter), you can implement another lifecycle method, getSnapshotBeforeUpdate, which allows you to compare your previous state and current state, and pass a return value to componentDidUpdate.

getSnapshotBeforeUpdate(prevProps, prevState) {
    // If relevant portion or prevState and state not equal return a 
    // value, else return null
}

componentDidUpdate(prevProps, prevState, snapshot) {
    // Check value of snapshot. If null, do not call your scroll 
    // function
}

like image 158
Greg Brodzik Avatar answered Nov 10 '22 14:11

Greg Brodzik