React-Meteor Chat Client: Scroll Down on New Message


#1

I’m trying to build a chat client with React and Meteor. One of the things I find to be relatively tricky is making sure the message box scrolls down when new message are added. I have the following components here:

  • a ChatContainer that houses a series of…
  • ChatMessage components.
  • as well as a ChatInput textarea where the user can send messages

I want the ChatContainer to scroll down in the following cases:

  1. When the user sends a message, and
  2. If the user is scrolled to the bottom of ChatContainer, then every new ChatMessage shall cause ChatContainer to scroll down.

Normally, in a non-reactive world, I’d poll continuously with AJAX. And I would simply call a scrollDown() function in the success callback. The problem with React is that each message component doesn’t know whether it’s new or not. I can’t call scrollDown() in componentDidMount() of every single chat message component or else it’ll fire many times on first load.

Another issue is that each chat message does not know whether or not the ChatContainer is scrolled down to the very bottom. This is all getting quite confusing for me even just typing this.

Any hints as to how I should structure this?


#2

So if you really boil it down the requirements (from what I understand) are that it should scroll every time there is a new chat, unless it’s the first load.

This kind of situtation in React is why flux/Redux was born. However, we don’t have to use it, we can just use the pattern. It would be nice to de-couple business logic from the view layer too.

We’ll stick with the Redux ‘naming’ for now because it will make it easier to switch later if you decide it’s needed. I’ll use ES6 and a module pattern to simulate the modules we’ll eventually have in Meteor. This way when it’s time to migrate you can change one line per file.

First let’s add a local data structure. I’ll use Session so that it’ll persist on hot code reloads, but you could use a Reactive-Dict as well. This file will just show future developers what the initial data is and provides a nice place for debugging.

// client/store.js setup initial data
Session.set('store:firstMessageTime', null);
Session.set('store:lastMessageTime', null);

Now we’ll add functions that will let us de-couple business logic from view logic and make that easy to unit test. Redux calls these ‘action creators’… functions that create an action object. However, we’re just using them to produce side effects since we’re not using ‘reducers’ and a redux store.

// client/actions.js

const Actions = {
  // only scroll down if the initial chats have finished rendering
  scrollChatDown(selector, firstTime, lastTime) {
    const HALF_SECOND = 500;

    if (lastTime - firstTime > HALF_SECOND) {
      $(selector).scrollTop($(selector).scrollTop() + 100);
    }
  },

  newChatMessage() {
    const firstChatTime = Session.get('store:firstMessageTime');
    const lastChatTime = Session.get('store:lastMessageTime');
    if (!firstChatTime) {
      Session.set('store:firstMessageTime', Date.now());
    }
    Session.set('store:lastMessageTime', Date.now());
    Actions.scrollChatDown('#chats', firstChatTime, lastChatTime);
  },
}
// simulate ES6 module exports
this.Actions = Actions;

Now in your react code you just need to fire off the #scrollChatDown function:

// react chat message class
...
componentDidMount() {
  Actions.newChatMessage();
}
...

#3

Thanks for the detailed reply @SkinnyGeek1010! I’ve read your posts in the Redux thread before but I guess I never really quite understood where to implement this. Now I guess I’ll finally be learning to put it into practice.

Just a curious question: Am I correct in saying that this is basically a way to write a throttling/debounce (can’t quite figure out which one) function in an Action store? Specifically, it seems similar to the throttling/debounce functions in lodash/underscore.

That being said, I do see a LOT of value in separating out the logic from the view into an Action object. That’ll make things a lot cleaner!

In any case, very cool! I’ll try to implement tomorrow after I get some sleep! Thanks again!


#4

It’s similar but it will only affect the first time specifically. Any subsequent times after the initial render won’t be limited… however if it’s a human adding posts then the debounce wouldn’t every be activated after the initial render.

You could def. use debounce and that would clean it up a bit.


#5

I’m working on something similar and I’m having some problems with this.

If I have the 10 most recent chat messages, and you only see the Load More Messages button when you scroll to the top of the chat window (i.e. msg X is at the top of the screen and is also the oldest msg). When you press load more, I want to maintain the scroll so that msg X is still at the top of the screen (or maybe shifted a bit down to indicate more have loaded) and has older messages above it that you can scroll to.

When I reactively load more, it always pushes the newer messages down and I’m still scrolled to the top. After loading, the scroll position should be somewhere near the middle.


#6

Just thinking about it intuitively, I would suggest the following steps:

  1. Save the current scroll position.
  2. Add the messages, and…
  3. On the success callback of having added the messages, calculate and restore the scroll position.

Building on top of the Redux example mentioned above, I would assume that this should all go into the Actions file.

However, if it’s trying to scroll before those messages have even been rendered, then this won’t work.

Maybe @SkinnyGeek1010 can chime in, he seems to be quite knowledgeable about what kinds of frameworks/structures work best for these kinds of situations.