Quickstart

A quick dive into getting started with Lore

Step 1: Freeze Pagination Data

In this step we'll add a timestamp to our pagination requests, to "freeze" the data for the list, and solve an error that occurs when we fetch a new page right after creating a tweet.

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

What's the problem?

Currently, if you create a tweet and refresh the page, the application works fine. And if you load more tweets, the application works fine.

But if you create a tweet and THEN load more tweets, you'll see this warning in the console:

Warning: flattenChildren(...): Encountered two children with the same key

This warning occurs because React is trying to render the same tweet twice. You'll also notice that only 4 new tweets show instead of the 5 we expect (which is the size of each page).

Why does this happen?

To illustrate why this happens, let's pretend we have an API with 10 tweets, shown below:

--- API page 1 ----
'tweet 10'
'tweet 9'
'tweet 8'
'tweet 7'
'tweet 6'
--- API page 2 ----
'tweet 5'
'tweet 4'
'tweet 3'
'tweet 2'
'tweet 1'

In this example, we're breaking the tweets up into two pages, with 5 tweets per page. We're also ordering the tweets chronologically, where tweet 10 is the newest tweet and tweet 1 is the oldest tweet.

Now let's say we fetch the first page of tweets to display to the user, so that our Feed on the client side looks like this:

--- Client page 1 ----
'tweet 10'
'tweet 9'
'tweet 8'
'tweet 7'
'tweet 6'

If you now create a new tweet, shown here as tweet 11, then the API response would now look like this:

--- API page 1 ----
'tweet 11'
'tweet 10'
'tweet 9'
'tweet 8'
'tweet 7'
--- API page 2 ----
'tweet 6'
'tweet 5'
'tweet 4'
'tweet 3'
'tweet 2'
--- API page 3 ----
'tweet 1'

Because the API is ordering tweets by their createdAt date, then it always puts the newest tweets on page 1, and the other tweets are pushed down the list. And in this case, tweet 6, which used to be on page 1, is now on page 2.

If you now load more tweets on the client side, you'll get the current page 2 from the API, and your list will look like this:

--- Client page 1 ----
'tweet 10'
'tweet 9'
'tweet 8'
'tweet 7'
'tweet 6'
--- Client page 2 ----
'tweet 6'
'tweet 5'
'tweet 4'
'tweet 3'
'tweet 2'

In this case, we now have tweet 6 showing up twice; once on page 1, where it used to be, and now again on page 2, where it currently is.

But since we're using the id of a tweet as the React key when rendering the list, we now have two tweets being rendered with the same key, and React throws a warning.

How do we fix this?

To solve this problem, we need a way to "freeze" the data in the API, so that the tweets on each page stay the same regardless of whether or not new tweets are created.

To do this, we're going to add a timestamp to our API request, so that our requests are relative to a specific point in time.

Add Timestamp to Feed

Open your Feed component and add initial state that includes a timestamp of when that component was mounted.

// src/components/Feed.js
...
getInitialState() {
  return {
    timestamp: new Date().toISOString()
  };
},
...
// src/components/Feed.js
...
constructor(props) {
  super(props);
  this.state = {
    timestamp: new Date().toISOString()
  };
}
...
// src/components/Feed.js
...
constructor(props) {
  super(props);
  this.state = {
    timestamp: new Date().toISOString()
  };
}
...

Fetching Tweets Relative to Timestamp

Now that we have the timestamp, we need to update our network requests to say "only give me tweets created BEFORE this timestamp".

To do that, the Sails API accepts a where object as a query parameter, that allows us to make some pretty specific requests. For example, to fetch all tweets created before a certain date, we can pass this object as a query parameter:

where: {
  createdAt: {
    '<=': timestamp
  }
}

As a network request, it looks like this:

GET http://localhost:1337/tweets?where={createdAt: {"<=": "2017-05-14T15:28:05-07:00"}}

Freeze the Pagination Data

To leverage this ability, open your Feed component and modify the render() method to look like this:

// src/components/Feed.js
render() {
  const { timestamp } = this.state;

  return (
    <div className="feed">
      ...
      <InfiniteScrollingList
        select={(getState) => {
          return getState('tweet.find', {
            where: {
              where: {
                createdAt: {
                  '<=': timestamp
                }
              }
            },
            pagination: {
              sort: 'createdAt DESC',
              page: 1
            }
          });
        }}
        ...
      />
    </div>
  );
}

Here we're modifying the select() callback by adding a where property to the getState() call, and providing the object we want sent to the API.

Visual Check-in

If everything went well, your application should now look like this (the same as before) but you should no longer see an error in the console when you paginate after creating a tweet.

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 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));
          }}
        />
      </div>
    );
  }

});
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
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));
          }}
        />
      </div>
    );
  }

}

export default Feed;
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
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));
          }}
        />
      </div>
    );
  }

}

export default Feed;

Next Steps

In the next section we'll learn how to display new tweets at the top of the Feed.