Quickstart

A quick dive into getting started with Lore

Step 3: Display Optimistic Tweets

In this step we'll update our strategy for displaying new tweets so that they appear immediately.

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

What's the problem?

While new tweets now show up at the top of the Feed, they only show up after the server confirms they exist. This means there's a delay between when a tweet is created and when it appears in the Feed.

While that's not necessarily a bad experience, it's not the one we want for this application.

How do we solve this?

We can solve this by using Lore's built-in support for optimistic updates to get tweets to appear immediately.

Display Optimistic Tweets

To start, open the Feed component and update the selectOther() callback to look like this:

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

In the code above, we've updated our where() filter to now include tweets that don't yet exist, and we did this by checking whether the tweet has an id.

Since the API server is responsible for assigning an id to resources, then we know that any tweets we have that don't have an id are optimistic; they represent data that is currently in the process of being created.

If you now refresh the browser and try to create a new tweet, the application will crash, and if you check the console, you'll see both a warning and an error:

Warning: Each child in an array or iterator should have a unique "key" prop.
...
Uncaught Error: Invalid call to 'getState('user.byId')'. Missing required attribute 'id'.

We'll fix the warning now, and solve the error in the next step.

Why the Warning Happens

The warning is happening because up until now, we've been using the id of our models as the key when rendering a list, which you can see in this code from the Feed component:

// src/components/Feed.js
<InfiniteScrollingList
  ...
  row={(tweet) => {
    return (
      <Tweet key={tweet.id} tweet={tweet} />
    );
  }}
/>

The problem is that optimistic data doesn't have an id, which means we're providing undefined as a value for the key. And React doesn't like that, so it issues a warning.

Update Key to Support Optimistic Data

To solve this, we need to provide an alternate key, and we can do that by using the cid for the model.

The cid property stands for "client id", and it exists for the sole purpose of supporting optimistic updates. Every model in Lore is assigned one, and the value is unique to that model. While there may be times a model won't have an id, it will always have a cid.

You can read more about the cid property here.

Update the key to look like this:

<InfiniteScrollingList
  ...
  row={(tweet) => {
    return (
      <Tweet key={tweet.id || tweet.cid} tweet={tweet} />
    );
  }}
/>

With that change in place, the warning should go away. In the next step we'll solve the error, and restore functionality to the application.

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.cid} 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 isOptimistic = !tweet.id;
                const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
                return isOptimistic || 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.cid} 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 isOptimistic = !tweet.id;
                const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
                return isOptimistic || 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.cid} 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 isOptimistic = !tweet.id;
                const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
                return isOptimistic || isNew;
              },
              sortBy: function(model) {
                return -moment(model.data.createdAt).unix();
              }
            });
          }}
        />
      </div>
    );
  }

}

export default Feed;

Next Steps

In the next section we'll fix the error and restore functionality to the application.