Quickstart

A quick dive into getting started with Lore

Step 2: Display New Tweets

In this step we'll display new tweets at the top of the Feed.

You can view the finished code for this step by checking out the optimistic.2 branch of the completed project.

What's the problem?

Currently, when we create new tweets, we have to refresh the page before they appear, which is not a very good user experience. A better experience would be to have new tweets automatically show up at the top of the Feed.

Why does this happen?

This happens because the application doesn't know what to do with the new tweets you create, since that answer is determined based on the experience you're creating.

For this type of experience, we want new tweets to appear at the top of the page, otherwise the application will feel "out of date" very quickly. But for other applications, it might not be as straight forward.

For example, should new tweets show up at all? If yes, where should they show up? At the top of the list? The bottom of the list? Should ALL new tweets show up, or just some? And if you DO want them to show up, should they show up immediately, or wait until the server confirms they'be been created?

Because of that uncertainty, Lore doesn't make any assumptions about what to do with "new data", and only gives you exactly what you ask for. And in this case, all we asked for was first page of tweets.

But it DOES give you some tools to easily describe the answers to those questions, and we'll be leveraging those tools in this step to shape the experience into what we want.

Chose the Location for New Tweets

The InfiniteScrollingList component we created earlier actually has a section that allows you to put "other" data at the top of the list. The relevant section of the render() method is shown below:

// src/components/InfiniteScrollingList.js
render() {
  ...
  return (
    <div>
      <ul className="media-list tweets">
        {other ? other.data.map(row) : null}
        // ...
      </ul>
      // ...
    </div>
  );
}

Display New Tweets

To get tweets to appear here, we simply need to provide the "other" data to the list. Open the Feed component and update the render() method to look like this:

// src/components/Feed.js
import moment from 'moment';
...
render() {
  ...
  return (
    <div className="feed">
      ...
      <InfiniteScrollingList
        ...
        selectOther={(getState) => {
          return getState('tweet.all', {
            where: function(tweet) {
              const isReal = tweet.id;
              const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
              return isReal && isNew;
            },
            sortBy: function(model) {
              return -moment(model.data.createdAt).unix();
            }
          });
        }}
      />
    </div>
  );
}

Here we've provided a selectOther() prop to the InfiniteScrollingList, which allows us to return an array of data that we want to be displayed at the top of the list.

To do this, we're introducing a new getState() mapping called tweet.all, which allows you to search through all tweets in the store, and extract a subset. To narrow down the results, there are two properties you can provide; where() and sortBy().

The where() method is where we specify our matching criteria. In this case, we want any tweets created AFTER the timestamp we're using to freeze the pagination data, as well as any tweets that don't yet have an id (which means the user just created them, and the server has not yet confirmed).

The sortBy() method allows us to express how we want those tweets to be ordered, and in this case, we want the newest tweets on top.

Try it Out

With that change in place, new tweets will show up at the top of the Feed shortly after you create them. You'll notice however that there's a shortly delay between the time you create a tweet and when it actually shows up in the list.

In the next step, we'll learn why this happens and to remove it.

Visual Check-in

If everything went well, your application should now look like this (exactly the same).

Code Changes

Below is a list of files modified during this step.

src/components/Feed.js

import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import _ from 'lodash';
import moment from 'moment';
import InfiniteScrollingList from './InfiniteScrollingList';
import Tweet from './Tweet';

export default createReactClass({
  displayName: 'Feed',

  getInitialState() {
    return {
      timestamp: new Date().toISOString()
    };
  },

  render() {
    const { timestamp } = this.state;

    return (
      <div className="feed">
        <h2 className="title">
          Feed
        </h2>
        <InfiniteScrollingList
          select={(getState) => {
            return getState('tweet.find', {
              where: {
                where: {
                  createdAt: {
                    '<=': timestamp
                  }
                }
              },
              pagination: {
                sort: 'createdAt DESC',
                page: 1
              }
            });
          }}
          row={(tweet) => {
            return (
              <Tweet key={tweet.id} tweet={tweet} />
            );
          }}
          refresh={(page, getState) => {
            return getState('tweet.find', page.query);
          }}
          selectNextPage={(lastPage, getState) => {
            const lastPageNumber = lastPage.query.pagination.page;

            return getState('tweet.find', _.defaultsDeep({
              pagination: {
                page: lastPageNumber + 1
              }
            }, lastPage.query));
          }}
          selectOther={(getState) => {
            return getState('tweet.all', {
              where: function(tweet) {
                const isReal = tweet.id;
                const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
                return isReal && isNew;
              },
              sortBy: function(model) {
                return -moment(model.data.createdAt).unix();
              }
            });
          }}
        />
      </div>
    );
  }

});
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import moment from 'moment';
import InfiniteScrollingList from './InfiniteScrollingList';
import Tweet from './Tweet';

class Feed extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      timestamp: new Date().toISOString()
    };
  }

  render() {
    const { timestamp } = this.state;

    return (
      <div className="feed">
        <h2 className="title">
          Feed
        </h2>
        <InfiniteScrollingList
          select={(getState) => {
            return getState('tweet.find', {
              where: {
                where: {
                  createdAt: {
                    '<=': timestamp
                  }
                }
              },
              pagination: {
                sort: 'createdAt DESC',
                page: 1
              }
            });
          }}
          row={(tweet) => {
            return (
              <Tweet key={tweet.id} tweet={tweet} />
            );
          }}
          refresh={(page, getState) => {
            return getState('tweet.find', page.query);
          }}
          selectNextPage={(lastPage, getState) => {
            const lastPageNumber = lastPage.query.pagination.page;

            return getState('tweet.find', _.defaultsDeep({
              pagination: {
                page: lastPageNumber + 1
              }
            }, lastPage.query));
          }}
          selectOther={(getState) => {
            return getState('tweet.all', {
              where: function(tweet) {
                const isReal = tweet.id;
                const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
                return isReal && isNew;
              },
              sortBy: function(model) {
                return -moment(model.data.createdAt).unix();
              }
            });
          }}
        />
      </div>
    );
  }

}

export default Feed;
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import moment from 'moment';
import InfiniteScrollingList from './InfiniteScrollingList';
import Tweet from './Tweet';

class Feed extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      timestamp: new Date().toISOString()
    };
  }

  render() {
    const { timestamp } = this.state;

    return (
      <div className="feed">
        <h2 className="title">
          Feed
        </h2>
        <InfiniteScrollingList
          select={(getState) => {
            return getState('tweet.find', {
              where: {
                where: {
                  createdAt: {
                    '<=': timestamp
                  }
                }
              },
              pagination: {
                sort: 'createdAt DESC',
                page: 1
              }
            });
          }}
          row={(tweet) => {
            return (
              <Tweet key={tweet.id} tweet={tweet} />
            );
          }}
          refresh={(page, getState) => {
            return getState('tweet.find', page.query);
          }}
          selectNextPage={(lastPage, getState) => {
            const lastPageNumber = lastPage.query.pagination.page;

            return getState('tweet.find', _.defaultsDeep({
              pagination: {
                page: lastPageNumber + 1
              }
            }, lastPage.query));
          }}
          selectOther={(getState) => {
            return getState('tweet.all', {
              where: function(tweet) {
                const isReal = tweet.id;
                const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
                return isReal && isNew;
              },
              sortBy: function(model) {
                return -moment(model.data.createdAt).unix();
              }
            });
          }}
        />
      </div>
    );
  }

}

export default Feed;

Next Steps

In the next section we'll remove the delay between creating a tweet and when it appears in the Feed.