React Chat Tutorial 2 Message History

React Chat Tutorial: Storing Message History (2/4)

This is Part Two of our four part series on building a chat app with React and PubNub. In Part One, we set up our app and covered basic messaging. If you haven’t yet, you’ll want to start with Part One before coming back here.

In this tutorial, we’ll use the PubNub Storage & Playback API (also know as history) to store historic messages, allow users to load some previous messages on app initialization, and to fetch more as they scroll up.

Up until this point, our app can send and receive messages, but there is no history of previous messages. This is because our data store is empty upon loading.

Managing State

React is a “view framework” only, and does not make decisions about the architecture of the application, or how state is managed.

So far for our chat app, we have a simple setup – a stateful root component and two stateless child components. The root component also holds all the functions for updating state. These functions and slices of state are then passed down to the child components. This approach works well for simple apps. However, we need a better approach as we begin to introduce more features and increase complexity.

One of the best ways to manage state is by using Redux – a state container that allows us to write much of our application as pure functions. This makes it much easier to test and understand our application. Also, it leads to a more thorough separation between views and the business logic.

Introduction to Redux Concepts

Redux is a simple but powerful tool for managing application state. Redux provides us with a central copy of global immutable state.  There are a number of reasons why we might want to leverage global immutable state with Redux:

  • Values, or state that our application depends on will come from a single source of “truth”.  This helps keep our views synchronized.
  • We can easily access all of our application’s data in a single location for debugging purposes as well as for other analytical purposes.
  • With Redux’s model we can replay all of the actions that created a given state
  • Other developers who know Redux’s architecture can more easily read our source code
  • Without a tool like Redux it’s a lot easier to make mistakes and create bugs that are hard to diagnose.
  • Redux is only a few kilobytes minified

To help us further avoid errors we’ll also use the Immutable library. Immutable provides us with data structures that simplify creating new immutable states which help avoid errors when working with Redux.

For additional resources on building React applications with Redux, I recommend Dan Abramov’s course on egghead.io, and for an overview you can check out this video from Rangle’s CTO Yuri Takhteyev.

Redux Basics

Redux is a pluggable data store that helps your application maintain a single consistent state that can be read globally; but only written when actions are triggered by the end user or the application.

When actions are triggered, they are dispatched to the store. The store then runs any installed middleware (plugins) and a reducer (we’ll explain reducers in a second) is run that updates the state. When all this is complete subscribers can access the updated store.

Redux Actions

Redux actions are simple JavaScript objects that must have a type property. For example:

let addMessage = {
    type: 'ADD_MESSAGE'
};

Above, we have an extremely simple action. Without a data payload, this action is not overly useful. To add this data, actions conventionally have a payload attribute.

let addMessage = {
    type: 'ADD_MESSAGE',
    payload: 'hello world'
};

Now that we’ve added a payload this action makes a lot more sense. We’ll be using an almost identical action later to add new chat messages to our application.

Reducers

Redux reducers must be pure functions that calculate the new state of an application. Pure functions are functions that have no “side effects” and always produce the same output for any given input. For example:

function add(a, b) {
   return a + b;
}

The above is a pure function because it affects nothing outside of itself. Given the same input, it will always return the same output. For example add(1, 1) will always return 2. Redux reducers are pure functions that are given a state and an action, which return a new state – emphasis on new.

Reducers are basically giant switch statements that decide how to calculate the new state based on actions. They might look something like this:

function messageReducer(state = [], action = {}) {
    switch (action.type) {
    case ADD_MESSAGE:
        return state.concat([action.payload]);
    default:
       return state;
}

The above is similar to a reducer we’ll be using in our application. Note how we returned a new array instead of modifying our state with a push call.

The example above is also slightly limiting. We’ve been worried about growing or scaling our application and a massive function that’s a switch statement is not going to do that for us. Fortunately, Redux ships with the concept of “slicing” state into small pieces. This lets us use combineReducers to compose many small reducers into one mega reducer. We can do this like so:

const rootReducer = combineReducers(
   messageReducer,
   someOtherReducer
);

The Redux Store

The Redux store is a simple container that stores data and provides a few methods. The store is one of the first things we set up in a Redux application. When we set up the store, we can optionally configure middleware (plugins) that will extend the store’s behavior.

During the store’s setup, we’ll also need to give it a reducer so that it knows how to calculate the new state. Setting up a store using what we know might look like this:

const store = createStore(rootReducer);

Typically, we’ll want to add and extend the “vanilla” Redux store with middleware. For that, Redux provides us with a compose function (just like a functional programming composes) which might look something like this:

const store = compose(
   someMiddleware,
   someOtherMiddleware
)(createStore)(rootReducer);

Store provides us a few methods that will become the backbone of our application’s data flow. We can call store.subscribe(listenerFunction) and when our state updates, our listenerFunction will be called. Our listener function will be given a copy of store.getState() which will give it access to the new state.

Finally, store.dispatch(action) will “apply” actions to the store which will result in a new state being calculated by the reducer(s). Once the new state is returned the subscriptions will trigger and the views are re-rendered.

Refactor

Let us first start by refactoring the app so that its behavior remains the same, but it will use Redux for state management. We are going to start by defining actions which are going to be used to put this data into the store. We’ll be able to do that using the simple type and payload properties. Reducers will then compute the new state and our App container will be automatically updated through bindings. Let’s implement!

You can continue with your code from where we left off in Part 1. Or, you can also get the starting point for this blog:

git clone https://github.com/pubnub/pubnub-react.git
git checkout tags/rev1

Actions

Since we only require actions to put data into the store, there are only two actions we’ll need: one to set the unique ID of the current user, and one to add a new message to the collection. Actions, as discussed earlier, are simple JavaScript objects. Managing type properties can be challenging if you’re not organized. We’ll be defining string constants that define our action types. This will make useing the reducers easier. Open the src/constants/index.js file and add the following two lines:

export const ADD_MESSAGE = 'App/ADD_MESSAGE';
export const SET_CURRENT_USERID = 'App/SET_CURRENT_USERID';

Next, we’ll create the actions that use these constants. First, create a folder in src/ called actions. Create a new file called src/actions/index.js and add the following line to import the constants:

import { ADD_MESSAGE, SET_CURRENT_USERID } from '../constants';

To keep things simple, the actions will be written to return only the object to be dispatched and the dispatching will be done in the App container. The first action we’ll write will set the current user’s ID. Our function will take a value for the ID and set that as the payload for the action, using the constant SET_CURRENT_USERID for the action type.

export function setCurrentUserID(userID) {
    return {
        type: SET_CURRENT_USERID,
        payload: userID,
    };
}

This simple method will let us conveniently store user ids. We’ll also need an action to addMessages to our history:

export function addMessage(message) {
    return {
        type: ADD_MESSAGE,
        payload: message,
    };
}

That’s it. This will allow us to save messages in the store. We’ll also be able to keep track of our current user.

Reducers

Create a new file in src/reducers/ called app.js and we’ll start off the file by adding the constants import:

import { ADD_MESSAGE, SET_CURRENT_USERID } from '../constants';

Next, we’ll import the fromJS function from ImmutableJS since it will allow us to ensure our reducer remains a pure function:

import { fromJS } from 'immutable';

Immutable’s fromJS function will let us convert JavaScript data structures into read-only objects that will prevent us from accidentally changing our store by reference. Before we make our reducers, it’s common practice to setup the initial state of their “slice” of the store. In our case let’s start with:

const INITIAL_STATE = fromJS({
    userID: 0,
    messages: [],
});

This is a very simple set of defaults. With this in place we can setup and export our reducer:

function appReducer(state = INITIAL_STATE, action = {}) {
    switch (action.type) {
    default:
        return state;
    }
}

export default appReducer;

At this point, the reducer does not do anything to change state. All it does is return the INITIAL_STATE over and over. Fortunately, for us, we already have two actions that we can add to our switch.

Let’s start by saving the userID field when the SET_CURRENT_USERID action is dispatched. Add this case to the appReducer switch statement:

case SET_CURRENT_USERID:
return state.update('userID', () => action.payload);

We just update the state using Immutable’s update method! Next, let’s deal with the ADD_MESSAGE case:

case ADD_MESSAGE:
    return state
    .update('messages', (messages) => messages.concat(action.payload));

This update is slightly trickier because we’re using concat to tack our new messages onto the messages List (Immutable’s version of an Array).

Configuring the Root Reducer

In the future, we’ll be splitting up our reducers to make them easier to manage. We’re already setup for this split, so we need to make sure our root reducer is configured to use the reducer we just made. Let’s edit src/reducers/index.js and add our app’s slice of state. First, we’ll import the reducer by adding to the top of the file:

import appReducer from './app';

Then we’ll modify the combineReducers call to include the appReducer:

const rootReducer = combineReducers({
    routing: routerReducer,
    app: appReducer,
});

Our reducer is now ready to be used by the application!

Container Binding

Finally, we can complete our Redux loop. We need to edit src/containers/app.js and import connect and our new actions by adding this to the top of the file:

import { connect } from 'react-redux';
import { setCurrentUserID, addMessage } from '../actions';

Redux itself is framework agnostic. Usually to use Redux with a framework you will have to use a helper library. In our case, we are using react-redux, which provides React bindings for Redux. We are going to use its connect function to connect the App container to the Redux store and actions. To do so, we need to modify the export statement at the bottom of the file to this:

export default connect(
    mapStateToProps,
    mapDispatchToProps,
)(App);

The mapDispatchToProps function allows us to bind the actions we created to the props of the App container.

function mapDispatchToProps(dispatch) {
    return {
        addMessage: (message) => dispatch(addMessage(message)),
        setUserID: (userID) => dispatch(setCurrentUserID(userID)),
    };
}

We will also bind part of our props object to stay in sync with updates from the store:

function mapStateToProps(state) {
return {
    history: state.app.get('messages').toJS(),
    userID: state.app.get('userID'),
};
}

Note: the use of .get() here is because state.app is an ImmutableJS object, and the way to obtain the current value is to use the .get(). We used the .toJS() on the messages array because ImmutableJS returns a List object when We use .get() on an array. List is an Immutable wrapper for arrays.

In our case, we don’t need Immutable objects beyond the store, so we convert the data back into regular javascript objects for ease of use. We’ve named the messages from our store as history, as that has a meaning in the context of the container.

At this point, the App container is going to receive four props: sendMessage, setUserID, history & userID. Update the App container to the following so that we can make use of these props:

class App extends React.Component {
    static propTypes = {
        history: React.PropTypes.array,
        userID: React.PropTypes.number,
        addMessage: React.PropTypes.func,
        setUserID: React.PropTypes.func,
    };

    componentDidMount() {
        const ID = Math.round(Math.random() * 1000000);
        this.props.setUserID(ID);
        this.PubNub = PUBNUB.init({
            publish_key: 'pub-c-199f8cfb-5dd3-470f-baa7-d6cb52929ca4',
            subscribe_key: 'sub-c-d2a5720a-1d1a-11e6-8b91-02ee2ddab7fe',
            ssl: (location.protocol.toLowerCase() === 'https:'),
        });
        this.PubNub.subscribe({
            channel: 'ReactChat',
            message: this.props.addMessage,
        });
    }

    render() {
        const { props, sendMessage } = this;
        return (<div></div>);
    }

    sendMessage = (message) => {
        this.PubNub.publish({
            channel: 'ReactChat',
            message: message,
       });
    }
}

You’ll notice that quite a few things changed here. Let’s go through them one-by-one:

  1. Since the state is now being managed by Redux, the App container no longer requires this.state.
  2. We added propTypes for the four props that are now bound to the container.
  3. We are setting the userID in the store by calling the setUserID action.
  4. Using the addMessage action to update the List of messages in the store.
  5. history & userID being passed down to components now come from props instead of state.

That’s it, we are now using Redux! You should take a moment to test your app to ensure it functions the same way as before.

Message History

Before you can use the history API, you will need to enable the Storage and Playback add-on. This can be done from your PubNub dashboard, or contact support@pubnub.com if you require further assistance.

Add History Action

In this section, we will walk through the process of adding a new action – ADD_HISTORY. This will allow us to update the List of messages in the store with any that we fetch from the history API.

Start by adding a constant to the constants file in src/constants/index.js:

export const ADD_HISTORY = 'App/ADD_HISTORY';

Next, open the actions in src/actions/index.js and update the import statement to include this new constant.

import { ADD_MESSAGE, SET_CURRENT_USERID, ADD_HISTORY } from '../constants';

Finally, add the following function to the end of the actions file:

export function addHistory(messages, timestamp) {
    return {
        type: ADD_HISTORY,
        payload: {
            messages,
            timestamp,
        },
    };
}

The addHistory action allows us to update the store’s history of messages. This action will be called from a callback method that will, in turn, be passed into the fetching history call.

You’ll notice that the addHistory function accepts two parameters: messages and timestamp. Here, messages is an array of message objects that the server is returning to us. The reducer will prepend these to the data history.

This timestamp will be used as the reference point for asking for more history. It is a PubNub time token string – containing the epoch time of the last messages in the array – multiplied by a large number. The timestamp will be returned to us from the server. Note, you should not attempt to store this value as an integer. It will cause an overflow during parsing and you don’t get the same number!

The payload for this action is different than previous actions. We are defining an object rather than passing in a primitive value. This is done because we need to pass in two pieces of data. To do so, we create an anonymous object using the shorthand syntax for property names.

Next, we need to update the reducer to implement this action type. Open the reducer file in /src/reducers/app.js and append the ADD_HISTORY constant to the import list:

import { ADD_MESSAGE, SET_CURRENT_USERID, ADD_HISTORY } from '../constants';

We are going to introduce a new field, timestamp, to the store. Therefore, the INITIAL_STATE needs to be updated with a with a default value for it. Add a lastMessageTimestamp key with the value of null to the INITIAL_STATE.

const INITIAL_STATE = fromJS({
userID: 0,
messages: [],
lastMessageTimestamp: null,
});

Finally, add the ADD_HISTORY case to the reducer switch statement:

case ADD_HISTORY:
return state
.update('messages', (messages) => messages.unshift(...action.payload.messages))
.update('lastMessageTimestamp', () => action.payload.timestamp);

This will replace the value for lastMessageTimestamp and update the messages to be prepended with the payload messages. Notice the use of the ES6 spread operator for inserting multiple messages in one go.

We can now update the App container to use this ADD_HISTORY action. Open src/containers/app.js and update the import statement for actions:

import { setCurrentUserID, addMessage, addHistory } from '../actions';

Add the addHistory function into the App container’s props by adding it to mapDispatchToProps:

function mapDispatchToProps(dispatch) {
return {
addMessage: (message) => dispatch(addMessage(message)),
setUserID: (userID) => dispatch(setCurrentUserID(userID)),
addHistory: (messages, timestamp) => dispatch(addHistory(messages, timestamp)),
};
}

We’ll also want to update mapStateToProps to include the new field, lastMessageTimestamp.

function mapStateToProps(state) {
return {
history: state.app.get('messages').toJS(),
userID: state.app.get('userID'),
lastMessageTimestamp: state.app.get('lastMessageTimestamp'),
};
}

Whenever we add something to one of the map to props functions, we need to add it to the container’s propTypes object too. Here’s what it should look like now:

static propTypes = {
history: React.PropTypes.array,
userID: React.PropTypes.number,
addMessage: React.PropTypes.func,
setUserID: React.PropTypes.func,
addHistory: React.PropTypes.func,
lastMessageTimestamp: React.PropTypes.string,
};

Fetch History

We can now create a fetchHistory method in the App container class (src/containers/app.js). This will call the PubNub history API – to fetch the message history – and use the addHistory function as the callback.

fetchHistory = () => {
const { props } = this;
this.PubNub.history({
channel: 'ReactChat',
count: 15,
start: props.lastMessageTimestamp,
callback: (data) => {
// data is Array(3), where index 0 is an array of messages
// and index 1 and 2 are start and end dates of the messages
props.addHistory(data[0], data[1]);
},
});
}

This fetchHistory function is just a wrapper for the PubNub history API. The channel is the same one we use to subscribe and publish. We are fetching 15 records at a time. You can, of course, configure this by changing the count value. The starting point for fetching messages is provided by the lastMessageTimestamp.

The callback, addHistory, will be invoked once the data is fetched from the server. The server returns an array with 3 values:

  • The first value is an array of message objects.
  • The second is the start timestamp.
  • The third is the end timestamp.

We are going to be fetching the message history in two cases:

  • When the application first loads.
  • Whenever the user scrolls to the top of the page.

History on Load

To fetch the history on load, we can call this.fetchHistory() from within the App container’s componentDidMount lifecycle hook. It should look something like this now:

componentDidMount() {
const ID = Math.round(Math.random() * 1000000);
this.props.setUserID(ID);
this.PubNub = PUBNUB.init({
publish_key: 'pub-c-199f8cfb-5dd3-470f-baa7-d6cb52929ca4',
subscribe_key: 'sub-c-d2a5720a-1d1a-11e6-8b91-02ee2ddab7fe',
ssl: (location.protocol.toLowerCase() === 'https:'),
});
this.PubNub.subscribe({
channel: 'ReactChat',
message: this.props.addMessage,
});
this.fetchHistory();
}

In the next section, we will implement fetching history on scroll by converting the ChatHistory component to an infinite scroll. For now, we are simply going to update the App container’s JSX to pass fetchHistory down into the ChatHistory component.

render() {
    const { props, sendMessage, fetchHistory } = this;
    return (<div></div>);
}

Before we continue, take a moment to test the app. You should now see a short message history on the first load.

Chat App with Scrolling

What We’ve Done So Far

Our application now has a robust system for managing state.  In order to make this happen we:

  • Setup our application to depend on the Redux library
  • Created an app reducer that knows how to calculate state based on action types
  • Created actions that can be dispatched to the store in order to trigger the reducers
  • Bound our view container to Redux so that when state changes, our views get recalculated
  • Bound our action creators to DOM events so that actions trigger on user input

We also have a blueprint for how we’ll manage state in the future:

  • We define slices of state using reducer functions.  Reducers are pure functions that execute a switch statement and produce new slices of state.
  • Actions are simple objects that are dispatched to the store.  Actions trigger reducers, which use the Action’s type property to calculate new state.
  • Action Creators are functions that make actions.  We can bind these creators to our view(s) to add functionality

Infinite Scrolling

Infinite scrolling is a technique wherein the application loads content continuously as the user scrolls up/down the page. We are now going to modify the ChatHistory component to implement infinite scrolling.

Open the ChatHistory component file: src/components/ChatHistory.js. We will start by updating the propTypes to include the fetchHistory function that we are now passing in. Append this to the propTypes:

fetchHistory: React.PropTypes.func,

Scroll Event Handler

We need to create an onScroll handler function which will bind to the window onscroll event. Start by defining the onScroll method for the ChatHistory component:

onScroll = () => {
// TODO: call fetchHistory when scrolled to the top
};

Update the render function to include onScroll in the destructuring:

const { props, onScroll } = this;

Next, we are going to update the JSX to bind to the scroll event. Add the onScroll event handler to the <ulclassName=”collection“>. Also, add a reference (ref). The updated the <ul> tag should look like this:

<ul className="collection" ref="messageList" onScroll={ onScroll }>

The ref attribute gives us access to this element, outside of the render function. We will use this in the onScroll function to get the current scroll position.  Generally speaking it’s best practice to avoid using the ref feature, in most cases it’s not necessary.  However, since infinite scroll depends on positional information that is only available from the DOM it’s a valid use case for the ref feature.

Right now, the application is allowing the user to scroll in the window and not the <ul> itself. This is because the <ul> has no height specified. It consumes as much height as it needs (100%). We will have to change this to limit scrolling in the <ul> only. This way we can get scrolling position relative to the <ul>’s viewport. Let us update the CSS for this. Open the src/styles/index.css file.

We are going to change the heights of the two components: ChatHistory and ChatInput. The ChatHistory component will take up 80% of the viewport and the ChatInput 20%.

    • Change footer’s height from 130px to 20vh
    • Add the following rules to the .collection class
      • height: 80vh;
      • margin: 0 0 130px 0;
      • overflow-y: scroll;
      • Lastly, prevent the body tag from scrolling by applying overflow: hidden;

The CSS should look now look like this:

/* ChatInput styles */footer {
color: white;
position: fixed;
padding: 0;
bottom: 0;
left: 0;
width: 100%;
height: 20vh;
}

footer .input-field input,
button {
border-color: #FFFFFF;
}

.chip {
font-style: italic;
}

/* ChatHistory styles */.collection {
margin: 0 0 130px 0;
overflow-y: scroll;
height: 80vh;
}

.message-date {
color: #585858;
}

body {
background: #EEEEEE;
overflow: hidden;
}

And the app should look something like this:

Chat App with CSS

Scroll to Fetch

Now that we have a scroll event handler, we can attach some business logic to it. The goal is to fetch history whenever the user scrolls to the top, i.e. scrollTop === 0.

Inside the ChatHistory component’s onScroll function, replace the comment with the following:

const { refs, props } = this;
const scrollTop = refs.messageList.scrollTop;
if (scrollTop === 0) {
props.fetchHistory();
}

 

Let’s see it in action.

Infinite Scroll in Action

Auto-Scrolling

You’ll notice that when the application first loads, the most recent messages are at the bottom, perhaps even off-screen. The user has to scroll down to see them. Let’s fix it so that the app automatically scrolls to the bottom as it receives new data.

We are going to do this in a two steps. First, we will create the scrollToBottom which is responsible for the auto-scrolling logic. Then we will invoke this function whenever data changes.

Add the scrollToBottom method to ChatHistory component class:

scrollToBottom = () => {
const { messageList } = this.refs;
const scrollHeight = messageList.scrollHeight;
const height = messageList.clientHeight;
const maxScrollTop = scrollHeight - height;
ReactDOM.findDOMNode(messageList).scrollTop = maxScrollTop > 0 ? maxScrollTop : 0;
}

In the first line we get access to the <ul> element via the ref we added earlier. In React, you access element refs from this.refs. Here, we are deconstructing this.refs to extract the specific element, messageList, that we require.

Next, we get the scroll height and the height of the <ul> element. We need this to calculate the maximum scroll top. This is the number of pixels the view needs scroll down to hit the bottom of the container. Finally, we assign the new maxScrollTop to the scrollTop.

The last line there uses ReactDOM. This means we will have to import it at the top of our file. Add the following line to the top, just below the react import:

import * as ReactDOM from 'react-dom';

The findDOMNode function allows us to get an element that is currently on the DOM. This is the actual live DOM node and not JSX.

When you use this.refs to access the node, you won’t be able to update scrollTop. This is because it is a reference from the last render cycle. As a general rule of thumb, you can use this.refs for read-only access to DOM nodes, When you need write access use ReactDOM.findDOMNode().

Now that we have defined the scrollToBottom function, we need to call it. We are going to use the componentDidUpdate lifecycle method for this. This lifecycle method is fired every time a component is updated. Therefore, whenever the data updates this function will be invoked. Add this to the ChatHistory component class:

componentDidUpdate() {
    this.scrollToBottom();
}

Now, when you reload the app, you’ll notice that the history list automatically scrolls to the bottom!

Chat App with Auto Scroll

Scroll to Bottom

We are going to focus on making this app a bit more user-friendly.

First, you might have noticed that when the app fetches new data (triggered by scrolling up) the view automatically scrolls to the bottom. This only needs to happen if the user has scrolled to the bottom and new data arrives. To fix this behaviour we need to add a check to see if the view is at the bottom, and only  call scrollToBottom if this is the case.

We need to create a variable, scrollAtBottom, which tracks whether the app needs to scroll to the bottom or not. To figure out if scrollAtBottom is true or false, we will compare the current scroll position to the bottom position. This will be done in the componentWillUpdate lifecycle method.

The componentWillUpdate method is called before a component renders. It is triggered whenever there is a change to the props that are passed into a component.

Add this to the ChatHistory component class:

componentWillUpdate(nextProps) {
this.historyChanged = nextProps.history.length !== this.props.history.length;
if (this.historyChanged) {
const { messageList } = this.refs;
const scrollPos = messageList.scrollTop;
const scrollBottom = (messageList.scrollHeight - messageList.clientHeight);
this.scrollAtBottom = (scrollBottom <= 0) || (scrollPos === scrollBottom);
}
}

Here, we start by checking if the history prop has changed or not. The componentWillUpdate method can be invoked when any of the props change, not just the message history. Therefore, we should limit the business logic to execute only when the history changes.

scrollAtBottom will be set to true if either of these two criteria are met:

  • If scrollBottom is less than or equal to zero. This would indicate there is not enough data to scroll.
  • Or if the scroll position is at the bottom.

We are also going to initialize this.scrollAtBottom for when the component first loads. On the first load, there is no data so it’s fair to assume that scroll position is at the bottom. Hence, we initialize scrollAtBottom as true by adding this line to the ChatHistory component class:

static scrollAtBottom = true;

The only thing left to do now is to apply the scrollAtBottom check before calling scrollToBottom(). For this, update the componentDidUpdate method:

componentDidUpdate() {
if (this.scrollAtBottom) {
this.scrollToBottom();
}
}

Another improvement we can make is to prevent the user from losing their scroll position when new data arrives. This can be fixed by calling scrollIntoView() on whatever the message they were last on.

Update the componentWillUpdate method by adding this snippet of code right after this.scrollAtBottom is set:

if (!this.scrollAtBottom) {
const numMessages = messageList.childNodes.length;
this.topMessage = numMessages === 0 ? null : messageList.childNodes[0];
}

Here we are checking to see if the scroll position is at the bottom or not. This is because the calculations only need to be done if the user has not scrolled to the bottom. Next, we check for the number of child nodes. This indicates that it is safe to access the first element from the childNodes array.

Then we update the componentDidMount method to scroll the topMessage into view, but only if the scroll position is not at the bottom. We also add a history check similar to what we did in componentWillUpdate. This is what the componentDidMount method should look like now:

componentDidUpdate() {
if (this.historyChanged) {
if (this.scrollAtBottom) {
this.scrollToBottom();
}
if (this.topMessage) {
ReactDOM.findDOMNode(this.topMessage).scrollIntoView();
}
}
}

The behavior now will be such that if the history changes and the user has scrolled to the bottom, then the app will auto-scroll to the new bottom (since new data was rendered). Otherwise, if the app will scroll, topMessage it into view.

 

User Friendly chat App

Next Steps

We now have a working chat application that can send and receive messages, and store those messages, complete with autoscroll and infinite scroll.

Next is Part 3, where we’ll add a realtime user list, allowing you to see which users are online and offline in realtime, and initiate a chat from it.

Language:

Try PubNub Today