Quickstart

A quick dive into getting started with Lore

Step 2: Display User Tweets

In this step we'll finish our support for filtering and add a component to view the user's tweets.

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

Add User Tweets Component

First we need a component to display the user's tweets. The behavior of this component is identical to the Feed, with one exception; we only want to display tweets the current user created. So start off by copying the Feed component and renaming it UserTweets.

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

export default createReactClass({
  displayName: 'UserTweets',

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

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

    return (
      <div className="feed">
        <h2 className="title">
          Feed
        </h2>
        <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();
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            });
          }}
          select={(getState) => {
            return getState('tweet.find', {
              where: {
                where: {
                  createdAt: {
                    '<=': timestamp
                  }
                }
              },
              pagination: {
                sort: 'createdAt DESC',
                page: 1,
                populate: 'user'
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            });
          }}
          row={(tweet) => {
            return (
              <Tweet key={tweet.id || tweet.cid} tweet={tweet} />
            );
          }}
          refresh={(page, getState) => {
            return getState('tweet.find', _.defaultsDeep({
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            }, page.query));
          }}
          selectNextPage={(lastPage, getState) => {
            const lastPageNumber = lastPage.query.pagination.page;

            return getState('tweet.find', _.defaultsDeep({
              pagination: {
                page: lastPageNumber + 1
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            }, lastPage.query));
          }}
        />
      </div>
    );
  }

});
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import PayloadStates from '../constants/PayloadStates';
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,
                populate: 'user'
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            });
          }}
          row={(tweet) => {
            return (
              <Tweet key={tweet.id || tweet.cid} tweet={tweet} />
            );
          }}
          refresh={(page, getState) => {
            return getState('tweet.find', _.defaultsDeep({
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            }, page.query));
          }}
          selectNextPage={(lastPage, getState) => {
            const lastPageNumber = lastPage.query.pagination.page;

            return getState('tweet.find', _.defaultsDeep({
              pagination: {
                page: lastPageNumber + 1
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            }, 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();
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            });
          }}
        />
      </div>
    );
  }

}

export default Feed;
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import PayloadStates from '../constants/PayloadStates';
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,
                populate: 'user'
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            });
          }}
          row={(tweet) => {
            return (
              <Tweet key={tweet.id || tweet.cid} tweet={tweet} />
            );
          }}
          refresh={(page, getState) => {
            return getState('tweet.find', _.defaultsDeep({
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            }, page.query));
          }}
          selectNextPage={(lastPage, getState) => {
            const lastPageNumber = lastPage.query.pagination.page;

            return getState('tweet.find', _.defaultsDeep({
              pagination: {
                page: lastPageNumber + 1
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            }, 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();
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            });
          }}
        />
      </div>
    );
  }

}

export default Feed;

Then update the render() method to include the changes below:

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

  return (
    <div className="feed">
      // ...
      <InfiniteScrollingList
        select={(getState) => {
          return getState('tweet.find', {
            where: {
              where: {
                createdAt: {
                  '<=': timestamp
                },
                user: Number(params.userId)
              }
            },
            pagination: {
              sort: 'createdAt DESC',
              page: 1,
              populate: 'user'
            },
            exclude: function(tweet) {
              return tweet.state === PayloadStates.DELETED;
            }
          });
        }}
        ...
        selectOther={(getState) => {
          return getState('tweet.all', {
            where: function(tweet) {
              const isOptimistic = !tweet.id;
              const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
              const isByUser = tweet.data.user === Number(params.userId);
              return (isOptimistic || isNew) && isByUser;
            },
            sortBy: function(model) {
              return -moment(model.data.createdAt).unix();
            },
            exclude: function(tweet) {
              return tweet.state === PayloadStates.DELETED;
            }
          });
        }}
      />
    </div>
  );
}

Here we've extracted params from props, which is automatically provided by react-router, so that we can get the userId provided in the URL.

For the selectOther() callback, we've added an isByUser variable, to detect whether the tweet is by the user with the userId. This will mean that only tweets created by the current user will show up on this page.

For the select() callback, we did something similar, added the user to the query parameters, so that the API will only return tweets created by that user. That means the Feed on the "My Tweets" page will consist only of tweets created by a single user.

Update Routes

Next, open routes.js and import the UserTweets component. Then register a new route for /users/:userId that will display that component.

// routes.js
...
import UserTweets from './src/components/UserTweets';

export default (
  <Route>
    <Route path="/login" component={Login} />
    <Route path="/logout" component={Logout} />
    <Route path="/auth/callback" component={AuthCallback} />

    <Route component={UserIsAuthenticated(Master)}>
      <Route path="/" component={Layout}>
        <IndexRoute component={Feed} />
        <Route path="users/:userId" component={UserTweets} />
      </Route>
    </Route>
  </Route>
);

With that change in place, refresh the browser and you'll now be able to view all the tweets or just the tweets created by the current user.

Visual Check-in

If everything went well, your application should now look like this when you select "My Tweets":

Code Changes

Below is a list of files modified during this step.

src/components/UserTweets.js

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

export default createReactClass({
  displayName: 'UserTweets',

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

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

    return (
      <div className="feed">
        <h2 className="title">
          Feed
        </h2>
        <InfiniteScrollingList
          select={(getState) => {
            return getState('tweet.find', {
              where: {
                where: {
                  createdAt: {
                    '<=': timestamp
                  },
                  user: Number(params.userId)
                }
              },
              pagination: {
                sort: 'createdAt DESC',
                page: 1,
                populate: 'user'
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            });
          }}
          row={(tweet) => {
            return (
              <Tweet key={tweet.id || tweet.cid} tweet={tweet} />
            );
          }}
          refresh={(page, getState) => {
            return getState('tweet.find', _.defaultsDeep({
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            }, page.query));
          }}
          selectNextPage={(lastPage, getState) => {
            const lastPageNumber = lastPage.query.pagination.page;

            return getState('tweet.find', _.defaultsDeep({
              pagination: {
                page: lastPageNumber + 1
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            }, lastPage.query));
          }}
          selectOther={(getState) => {
            return getState('tweet.all', {
              where: function(tweet) {
                const isOptimistic = !tweet.id;
                const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
                const isByUser = tweet.data.user === Number(params.userId);
                return (isOptimistic || isNew) && isByUser;
              },
              sortBy: function(model) {
                return -moment(model.data.createdAt).unix();
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            });
          }}
        />
      </div>
    );
  }

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

class UserTweets extends React.Component {

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

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

    return (
      <div className="feed">
        <h2 className="title">
          Feed
        </h2>
        <InfiniteScrollingList
          select={(getState) => {
            return getState('tweet.find', {
              where: {
                where: {
                  createdAt: {
                    '<=': timestamp
                  },
                  user: Number(params.userId)
                }
              },
              pagination: {
                sort: 'createdAt DESC',
                page: 1,
                populate: 'user'
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            });
          }}
          row={(tweet) => {
            return (
              <Tweet key={tweet.id || tweet.cid} tweet={tweet} />
            );
          }}
          refresh={(page, getState) => {
            return getState('tweet.find', _.defaultsDeep({
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            }, page.query));
          }}
          selectNextPage={(lastPage, getState) => {
            const lastPageNumber = lastPage.query.pagination.page;

            return getState('tweet.find', _.defaultsDeep({
              pagination: {
                page: lastPageNumber + 1
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            }, lastPage.query));
          }}
          selectOther={(getState) => {
            return getState('tweet.all', {
              where: function(tweet) {
                const isOptimistic = !tweet.id;
                const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
                const isByUser = tweet.data.user === Number(params.userId);
                return (isOptimistic || isNew) && isByUser;
              },
              sortBy: function(model) {
                return -moment(model.data.createdAt).unix();
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            });
          }}
        />
      </div>
    );
  }

}

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

class UserTweets extends React.Component {

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

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

    return (
      <div className="feed">
        <h2 className="title">
          Feed
        </h2>
        <InfiniteScrollingList
          select={(getState) => {
            return getState('tweet.find', {
              where: {
                where: {
                  createdAt: {
                    '<=': timestamp
                  },
                  user: Number(params.userId)
                }
              },
              pagination: {
                sort: 'createdAt DESC',
                page: 1,
                populate: 'user'
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            });
          }}
          row={(tweet) => {
            return (
              <Tweet key={tweet.id || tweet.cid} tweet={tweet} />
            );
          }}
          refresh={(page, getState) => {
            return getState('tweet.find', _.defaultsDeep({
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            }, page.query));
          }}
          selectNextPage={(lastPage, getState) => {
            const lastPageNumber = lastPage.query.pagination.page;

            return getState('tweet.find', _.defaultsDeep({
              pagination: {
                page: lastPageNumber + 1
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            }, lastPage.query));
          }}
          selectOther={(getState) => {
            return getState('tweet.all', {
              where: function(tweet) {
                const isOptimistic = !tweet.id;
                const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
                const isByUser = tweet.data.user === Number(params.userId);
                return (isOptimistic || isNew) && isByUser;
              },
              sortBy: function(model) {
                return -moment(model.data.createdAt).unix();
              },
              exclude: function(tweet) {
                return tweet.state === PayloadStates.DELETED;
              }
            });
          }}
        />
      </div>
    );
  }

}

export default UserTweets;

routes.js

import React from 'react';
import { Route, IndexRoute, Redirect } from 'react-router';

/**
 * Wrapping the Master component with this decorator provides an easy way
 * to redirect the user to a login experience if we don't know who they are.
 */
import UserIsAuthenticated from './src/decorators/UserIsAuthenticated';

/**
 * Routes are used to declare your view hierarchy
 * See: https://github.com/ReactTraining/react-router/blob/v3/docs/API.md
 */
import Master from './src/components/Master';
import Layout from './src/components/Layout';
import Feed from './src/components/Feed';
import Login from './src/components/Login';
import AuthCallback from './src/components/AuthCallback';
import Logout from './src/components/Logout';
import UserTweets from './src/components/UserTweets';

export default (
  <Route>
    <Route path="/login" component={Login} />
    <Route path="/logout" component={Logout} />
    <Route path="/auth/callback" component={AuthCallback} />

    <Route component={UserIsAuthenticated(Master)}>
      <Route path="/" component={Layout}>
        <IndexRoute component={Feed} />
        <Route path="users/:userId" component={UserTweets} />
      </Route>
    </Route>
  </Route>
);

Next Steps

In the next section we'll we'll add support for WebSockets.