0

I'm writing a chat application using reactjs and I am trying to figure out how to change the focus to the latest message every time a new message is added to the array. My react chat window looks like this:

<ChatHeader />
<Messages datas={this.state.datas} />
<ChatFooter sendMessage={this.sendMessage} />

and my Messages component looks like this:

var Messages = React.createClass({
  render: function() {
    // Loop through the list of messages and create array of Row components
    var Rows = this.props.datas.map(function(data) {
      return (
        <Row username={data.username} message={data.message} /> 
      )
    });

    return (
        {Rows}
    );
  }
});

So basically every time my main chat window rerenders, I want messages to be displayed with the list scrolled all the way down (showing the most recent messages - or the last few things in the Rows array). I found a few questions about shifting focus like this one here but when I try shifting focus to messages it brings me to the top of the list. Ideally I'd like to add something like this:

   var Rows = this.props.datas.map(function(data) {
      return (
        <Row username={data.username} message={data.message} /> 
      )
    });

//    Rows[Rows.length - 1].ref = "last";

so I can then focus on the last element in the array but I haven't been able to find a way to set references like this. All the ways I've seen show setting the reference inside the tag. Does a way to assign references like this exist?

I also thought about giving each row a unique id number and incrementing that number for each new message, ,keeping track of the largest number and then using that to reference the last element but it seems like there must be a better way. What is the best way to accomplish this?

Community
  • 1
  • 1
user3282276
  • 3,674
  • 8
  • 32
  • 48
  • 1
    What do you mean by focus? In the HTML world focus means an input is receiving keyboard control (like when you click inside a textarea). Are you just wanting it to be scrolled to? – TheZanke Jun 23 '16 at 17:02
  • @Alex'TheZanke'Howard Yes I just want the newest message to be automatically scrolled to – user3282276 Jun 23 '16 at 17:04

1 Answers1

0

We're going to have to utilize what is considered an anti-pattern in React's book and use ReactDOM.findDOMNode which they typically frown upon.

Essentially, what we are going to do is create a method on the Messages class called onRowRender and bind it to this in the Messages constructor. We'll use the optional second argument of map to keep track of how many rows have been generated, and if it's the last row we'll pass that onRowRendered method to the Row component using a prop we'll call onRender.

In the componentDidMount of each Row component we will then check to see if this.props.onRender exists and if it does then that means it is the last row so we call this.props.onRender and pass it this which is the Row component in question.

Since we are doing all of this after the component has mounted it means that we will be able to use ReactDOM.findDOMNode to get the raw javascript DOM element for that rendered component. We do this in the handler we made on the parent Messages class.

After all of that is said and done we get the top offset of the DOMNode in question and we also get it's parentNode and then set the topOffset of the parentNode to be equal to the top offset.

NOTE: I am using the es6 class syntax in my example but it should work with React.createClass too.

class Messages extends React.Component {
  constructor() {
    super();
    this.onRowRender = this.onRowRender.bind(this);
  }

  onRowRender(row) {
    var rowDOM = ReactDOM.findDOMNode(row);
    var offsets = rowDOM.getClientRects()[0];
    var parent = rowDOM.parentNode;
    parent.scrollTop = offsets.top;
  }

  render() {
    return (
      <div className='messages'>
        {this.props.datas.map((data, i) => {
          return (
            <Row
              key={data.key}
              username={data.username}
              message={data.message}
              onRender={i === this.props.datas.length - 1 ? this.onRowRender : null}
            />
          );
        })}
      </div>
    );
  }
}

class Row extends React.Component {
  componentDidMount() {
    if (this.props.onRender) this.props.onRender(this);
  }

  render() {
    return (
      <div>
        {this.props.message}&nbsp;&nbsp;&nbsp;by: {this.props.username}
      </div>
    );
  }
}

var datas = [
  {
    key: 1,
    username: 'test user #1',
    message: 'test message #1',
  }, {
    key: 2,
    username: 'test user #2',
    message: 'test message #2',
  }, {
    key: 3,
    username: 'test user #3',
    message: 'test message #3',
  }, {
    key: 4,
    username: 'test user #4',
    message: 'test message #4',
  }, {
    key: 5,
    username: 'test user #5',
    message: 'test message #5',
  },
];

ReactDOM.render(
  <Messages datas={datas} />,
  document.getElementById('root')
);

Obligatory jsfiddle: https://jsfiddle.net/69z2wepo/46701/

Alternatively you could just set the scrollTop of the parent to be it's own scroll height by changing the onRender prop of your Rows to be simply onRender={this.onRowRender} and then changing onRowRender to be:

onRowRender(row) {
  var rowDOM = ReactDOM.findDOMNode(row);
  var parent = rowDOM.parentNode;
  parent.scrollTop = parent.scrollHeight;
}

This will scroll it to the bottom any time a new Row is mounted. However, by doing it the way I initially demonstrated it'll make sure that if the last Row is larger than the total height of the Messages component it will only scroll to the top of that Row, instead of the bottom the whole list. (I hope that makes sense)

TheZanke
  • 1,026
  • 8
  • 12