Quickstart

A quick dive into getting started with Lore

Step 1: Normalize Tweet Response

In this step we'll ask the API to populate the user for each tweet, and then teach the application how to normalize it.

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

Request Nested Data

To start, we need to update our network request to ask the API to populate the user. Open the Feed component and modify the select() callback to look like this (adding the populate parameter to the pagination properties):

// src/components/Feed.js
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;
    }
  });
}}

If you refresh the page, you'll notice it no longer displays correctly.

Why is it not working?

To understand why the application is broken, open the developer tools and take a look at the network requests.

If you locate the API call to fetch the first page of tweets, you'll see that it now looks like http://localhost:1337/tweets?page=1&populate=user, which is what we wanted, and you can confirm the user data is nested in the response.

But you'll also see that not only are we still trying to fetch the user, but that the first call to fetch a user for a tweet looks like http://localhost:1337/users/%5Bobject%20Object%5D instead of http://localhost:1337/users/1.

The reason for the strange looking API call is because tweet.data.user used to be a number like 1, but now it's an object. And since we haven't taught Lore how to process nested data, it just passes it along to the component.

Specify Nested Relationships

To fix this issue we need to tell Lore that tweet resources may contain nested user data, and that this data should be broken out and converted to a user model.

To do that open src/models/tweet.js and add another attribute for the user field, specifying the type as a model and the associated model to be a user:

export default {

  attributes: {
    user: {
      type: 'model',
      model: 'user'
    }
  },

  ...
};

With this change in place, refresh the browser you this time the application should load properly and you should see only two network requests instead of 6:

[1] XHR finished loading: GET "http://localhost:1337/user"
[2] XHR finished loading: GET "http://localhost:1337/tweets?page=1&populate=user"

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/models/tweet.js

const fields = {
  data: {
    text: ''
  },
  validators: {
    text: [function(value) {
      if (!value) {
        return 'This field is required';
      }
    }]
  },
  fields: [
    {
      key: 'text',
      type: 'text',
      props: {
        label: 'Message',
        placeholder: "What's happening?"
      }
    }
  ]
};

export default {

  attributes: {
    user: {
      type: 'model',
      model: 'user'
    }
  },

  dialogs: {
    create: fields,
    update: fields
  }

}

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 PayloadStates from '../constants/PayloadStates';
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,
                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>
    );
  }

});
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 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 _ from 'lodash';
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;

Next Steps

In the next section we'll add the ability to view all tweets or just the ones you created.