Quickstart

A quick dive into getting started with Lore

Step 2: Create Infinite Scrolling List

In this step we'll create the List component we'll need for infinite scrolling.

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

Create InfiniteScrollingList Component

The second component we'll create will be called InfiniteScrollingList, and it will be responsible for displaying our list of tweets, as well as merging all the pages of data into a single array.

Create the component by running the following command:

lore generate component InfiniteScrollingList

Then modify the file to look like this:

import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { getState } from 'lore-hook-connect';
import PayloadStates from '../constants/PayloadStates';
import LoadMoreButton from './LoadMoreButton';

export default createReactClass({
  displayName: 'InfiniteScrollingList',

  propTypes: {
    row: PropTypes.func.isRequired,
    select: PropTypes.func.isRequired,
    selectNextPage: PropTypes.func,
    refresh: PropTypes.func,
    selectOther: PropTypes.func,
    exclude: PropTypes.func
  },

  getDefaultProps() {
    return {
      exclude: function(model) {
        return false;
      }
    };
  },

  getInitialState() {
    return {
      other: null,
      pages: []
    };
  },

  // fetch first page
  componentWillMount() {
    const { select, selectOther } = this.props;
    const nextState = this.state;

    nextState.pages.push(select(getState));

    if (selectOther) {
      nextState.other = selectOther(getState);
    }

    this.setState(nextState);
  },

  // refresh data in all pages
  componentWillReceiveProps(nextProps) {
    const { refresh, selectOther } = this.props;
    const { pages } = this.state;
    const nextState = {};

    if (refresh) {
      nextState.pages = pages.map(function(page) {
        return refresh(page, getState);
      });
    }

    if (selectOther) {
      nextState.other = selectOther(getState);
    }

    this.setState(nextState);
  },

  onLoadMore() {
    const { selectNextPage } = this.props;
    const { pages } = this.state;
    const lastPage = pages[pages.length - 1];

    pages.push(selectNextPage(lastPage, getState));

    this.setState({
      pages: pages
    });
  },

  render() {
    const { row, exclude, selectNextPage } = this.props;
    const { pages, other } = this.state;
    const numberOfPages = pages.length;
    const firstPage = pages[0];
    const lastPage = pages[pages.length - 1];

    // if we only have one page, and it's fetching, then it's the initial
    // page load so let the user know we're loading the data
    if (numberOfPages === 1 && lastPage.state === PayloadStates.FETCHING) {
      return (
        <div className="loader" />
      );
    }

    return (
      <div>
        <ul className="media-list tweets">
          {other ? other.data.map(row) : null}
          {_.flatten(pages.map((models) => {
            return _.filter(models.data, (model) => {
              return !exclude(model);
            }).map(row);
          }))}
        </ul>
        {selectNextPage ? (
          <LoadMoreButton
            lastPage={lastPage}
            onLoadMore={this.onLoadMore}
            nextPageMetaField="nextPage"
          />
        ) : null}
      </div>
    );
  }

});
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { getState } from 'lore-hook-connect';
import PayloadStates from '../constants/PayloadStates';
import LoadMoreButton from './LoadMoreButton';

class InfiniteScrollingList extends React.Component {

  constructor(props) {
    super(props);

    // set initial state
    this.state = {
      other: null,
      pages: []
    };

    // bind custom methods
    this.onLoadMore = this.onLoadMore.bind(this);
  }

  // fetch first page
  componentWillMount() {
    const { select, selectOther } = this.props;
    const nextState = this.state;

    nextState.pages.push(select(getState));

    if (selectOther) {
      nextState.other = selectOther(getState);
    }

    this.setState(nextState);
  }

  // refresh data in all pages
  componentWillReceiveProps(nextProps) {
    const { refresh, selectOther } = this.props;
    const { pages } = this.state;
    const nextState = {};

    if (refresh) {
      nextState.pages = pages.map(function(page) {
        return refresh(page, getState);
      });
    }

    if (selectOther) {
      nextState.other = selectOther(getState);
    }

    this.setState(nextState);
  }

  onLoadMore() {
    const { selectNextPage } = this.props;
    const { pages } = this.state;
    const lastPage = pages[pages.length - 1];

    pages.push(selectNextPage(lastPage, getState));

    this.setState({
      pages: pages
    });
  }

  render() {
    const { row, exclude, selectNextPage } = this.props;
    const { pages, other } = this.state;
    const numberOfPages = pages.length;
    const firstPage = pages[0];
    const lastPage = pages[pages.length - 1];

    // if we only have one page, and it's fetching, then it's the initial
    // page load so let the user know we're loading the data
    if (numberOfPages === 1 && lastPage.state === PayloadStates.FETCHING) {
      return (
        <div className="loader" />
      );
    }

    return (
      <div>
        <ul className="media-list tweets">
          {other ? other.data.map(row) : null}
          {_.flatten(pages.map((models) => {
            return _.filter(models.data, (model) => {
              return !exclude(model);
            }).map(row);
          }))}
        </ul>
        {selectNextPage ? (
          <LoadMoreButton
            lastPage={lastPage}
            onLoadMore={this.onLoadMore}
            nextPageMetaField="nextPage"
          />
        ) : null}
      </div>
    );
  }
}

InfiniteScrollingList.propTypes = {
  row: PropTypes.func.isRequired,
  select: PropTypes.func.isRequired,
  selectNextPage: PropTypes.func,
  refresh: PropTypes.func,
  selectOther: PropTypes.func,
  exclude: PropTypes.func
};

InfiniteScrollingList.defaultProps = {
  exclude: function(model) {
    return false;
  }
};

export default InfiniteScrollingList;
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { getState } from 'lore-hook-connect';
import PayloadStates from '../constants/PayloadStates';
import LoadMoreButton from './LoadMoreButton';

class InfiniteScrollingList extends React.Component {

  static propTypes = {
    row: PropTypes.func.isRequired,
    select: PropTypes.func.isRequired,
    selectNextPage: PropTypes.func,
    refresh: PropTypes.func,
    selectOther: PropTypes.func,
    exclude: PropTypes.func
  };

  static defaultProps = {
    exclude: function(model) {
      return false;
    }
  };

  constructor(props) {
    super(props);

    // set initial state
    this.state = {
      other: null,
      pages: []
    };

    // bind custom methods
    this.onLoadMore = this.onLoadMore.bind(this);
  }

  // fetch first page
  componentWillMount() {
    const { select, selectOther } = this.props;
    const nextState = this.state;

    nextState.pages.push(select(getState));

    if (selectOther) {
      nextState.other = selectOther(getState);
    }

    this.setState(nextState);
  }

  // refresh data in all pages
  componentWillReceiveProps(nextProps) {
    const { refresh, selectOther } = this.props;
    const { pages } = this.state;
    const nextState = {};

    if (refresh) {
      nextState.pages = pages.map(function(page) {
        return refresh(page, getState);
      });
    }

    if (selectOther) {
      nextState.other = selectOther(getState);
    }

    this.setState(nextState);
  }

  onLoadMore() {
    const { selectNextPage } = this.props;
    const { pages } = this.state;
    const lastPage = pages[pages.length - 1];

    pages.push(selectNextPage(lastPage, getState));

    this.setState({
      pages: pages
    });
  }

  render() {
    const { row, exclude, selectNextPage } = this.props;
    const { pages, other } = this.state;
    const numberOfPages = pages.length;
    const firstPage = pages[0];
    const lastPage = pages[pages.length - 1];

    // if we only have one page, and it's fetching, then it's the initial
    // page load so let the user know we're loading the data
    if (numberOfPages === 1 && lastPage.state === PayloadStates.FETCHING) {
      return (
        <div className="loader" />
      );
    }

    return (
      <div>
        <ul className="media-list tweets">
          {other ? other.data.map(row) : null}
          {_.flatten(pages.map((models) => {
            return _.filter(models.data, (model) => {
              return !exclude(model);
            }).map(row);
          }))}
        </ul>
        {selectNextPage ? (
          <LoadMoreButton
            lastPage={lastPage}
            onLoadMore={this.onLoadMore}
            nextPageMetaField="nextPage"
          />
        ) : null}
      </div>
    );
  }
}

export default InfiniteScrollingList;

The component above is pretty generic. The props that start with select* are functions that we'll be providing to describe what data we want to display, and the row prop is a function that we'll provide to tell the List how to render each item in the array.

Visual Check-in

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

Code Changes

Below is a list of files modified during this step.

src/decorators/InfiniteScrolling.js

import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { getState } from 'lore-hook-connect';
import PayloadStates from '../constants/PayloadStates';
import LoadMoreButton from './LoadMoreButton';

export default createReactClass({
  displayName: 'InfiniteScrollingList',

  propTypes: {
    row: PropTypes.func.isRequired,
    select: PropTypes.func.isRequired,
    selectNextPage: PropTypes.func,
    refresh: PropTypes.func,
    selectOther: PropTypes.func,
    exclude: PropTypes.func
  },

  getDefaultProps() {
    return {
      exclude: function(model) {
        return false;
      }
    };
  },

  getInitialState() {
    return {
      other: null,
      pages: []
    };
  },

  // fetch first page
  componentWillMount() {
    const { select, selectOther } = this.props;
    const nextState = this.state;

    nextState.pages.push(select(getState));

    if (selectOther) {
      nextState.other = selectOther(getState);
    }

    this.setState(nextState);
  },

  // refresh data in all pages
  componentWillReceiveProps(nextProps) {
    const { refresh, selectOther } = this.props;
    const { pages } = this.state;
    const nextState = {};

    if (refresh) {
      nextState.pages = pages.map(function(page) {
        return refresh(page, getState);
      });
    }

    if (selectOther) {
      nextState.other = selectOther(getState);
    }

    this.setState(nextState);
  },

  onLoadMore() {
    const { selectNextPage } = this.props;
    const { pages } = this.state;
    const lastPage = pages[pages.length - 1];

    pages.push(selectNextPage(lastPage, getState));

    this.setState({
      pages: pages
    });
  },

  render() {
    const { row, exclude, selectNextPage } = this.props;
    const { pages, other } = this.state;
    const numberOfPages = pages.length;
    const firstPage = pages[0];
    const lastPage = pages[pages.length - 1];

    // if we only have one page, and it's fetching, then it's the initial
    // page load so let the user know we're loading the data
    if (numberOfPages === 1 && lastPage.state === PayloadStates.FETCHING) {
      return (
        <div className="loader" />
      );
    }

    return (
      <div>
        <ul className="media-list tweets">
          {other ? other.data.map(row) : null}
          {_.flatten(pages.map((models) => {
            return _.filter(models.data, (model) => {
              return !exclude(model);
            }).map(row);
          }))}
        </ul>
        {selectNextPage ? (
          <LoadMoreButton
            lastPage={lastPage}
            onLoadMore={this.onLoadMore}
            nextPageMetaField="nextPage"
          />
        ) : null}
      </div>
    );
  }

});
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { getState } from 'lore-hook-connect';
import PayloadStates from '../constants/PayloadStates';
import LoadMoreButton from './LoadMoreButton';

class InfiniteScrollingList extends React.Component {

  constructor(props) {
    super(props);

    // set initial state
    this.state = {
      other: null,
      pages: []
    };

    // bind custom methods
    this.onLoadMore = this.onLoadMore.bind(this);
  }

  // fetch first page
  componentWillMount() {
    const { select, selectOther } = this.props;
    const nextState = this.state;

    nextState.pages.push(select(getState));

    if (selectOther) {
      nextState.other = selectOther(getState);
    }

    this.setState(nextState);
  }

  // refresh data in all pages
  componentWillReceiveProps(nextProps) {
    const { refresh, selectOther } = this.props;
    const { pages } = this.state;
    const nextState = {};

    if (refresh) {
      nextState.pages = pages.map(function(page) {
        return refresh(page, getState);
      });
    }

    if (selectOther) {
      nextState.other = selectOther(getState);
    }

    this.setState(nextState);
  }

  onLoadMore() {
    const { selectNextPage } = this.props;
    const { pages } = this.state;
    const lastPage = pages[pages.length - 1];

    pages.push(selectNextPage(lastPage, getState));

    this.setState({
      pages: pages
    });
  }

  render() {
    const { row, exclude, selectNextPage } = this.props;
    const { pages, other } = this.state;
    const numberOfPages = pages.length;
    const firstPage = pages[0];
    const lastPage = pages[pages.length - 1];

    // if we only have one page, and it's fetching, then it's the initial
    // page load so let the user know we're loading the data
    if (numberOfPages === 1 && lastPage.state === PayloadStates.FETCHING) {
      return (
        <div className="loader" />
      );
    }

    return (
      <div>
        <ul className="media-list tweets">
          {other ? other.data.map(row) : null}
          {_.flatten(pages.map((models) => {
            return _.filter(models.data, (model) => {
              return !exclude(model);
            }).map(row);
          }))}
        </ul>
        {selectNextPage ? (
          <LoadMoreButton
            lastPage={lastPage}
            onLoadMore={this.onLoadMore}
            nextPageMetaField="nextPage"
          />
        ) : null}
      </div>
    );
  }
}

InfiniteScrollingList.propTypes = {
  row: PropTypes.func.isRequired,
  select: PropTypes.func.isRequired,
  selectNextPage: PropTypes.func,
  refresh: PropTypes.func,
  selectOther: PropTypes.func,
  exclude: PropTypes.func
};

InfiniteScrollingList.defaultProps = {
  exclude: function(model) {
    return false;
  }
};

export default InfiniteScrollingList;
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { getState } from 'lore-hook-connect';
import PayloadStates from '../constants/PayloadStates';
import LoadMoreButton from './LoadMoreButton';

class InfiniteScrollingList extends React.Component {

  static propTypes = {
    row: PropTypes.func.isRequired,
    select: PropTypes.func.isRequired,
    selectNextPage: PropTypes.func,
    refresh: PropTypes.func,
    selectOther: PropTypes.func,
    exclude: PropTypes.func
  };

  static defaultProps = {
    exclude: function(model) {
      return false;
    }
  };

  constructor(props) {
    super(props);

    // set initial state
    this.state = {
      other: null,
      pages: []
    };

    // bind custom methods
    this.onLoadMore = this.onLoadMore.bind(this);
  }

  // fetch first page
  componentWillMount() {
    const { select, selectOther } = this.props;
    const nextState = this.state;

    nextState.pages.push(select(getState));

    if (selectOther) {
      nextState.other = selectOther(getState);
    }

    this.setState(nextState);
  }

  // refresh data in all pages
  componentWillReceiveProps(nextProps) {
    const { refresh, selectOther } = this.props;
    const { pages } = this.state;
    const nextState = {};

    if (refresh) {
      nextState.pages = pages.map(function(page) {
        return refresh(page, getState);
      });
    }

    if (selectOther) {
      nextState.other = selectOther(getState);
    }

    this.setState(nextState);
  }

  onLoadMore() {
    const { selectNextPage } = this.props;
    const { pages } = this.state;
    const lastPage = pages[pages.length - 1];

    pages.push(selectNextPage(lastPage, getState));

    this.setState({
      pages: pages
    });
  }

  render() {
    const { row, exclude, selectNextPage } = this.props;
    const { pages, other } = this.state;
    const numberOfPages = pages.length;
    const firstPage = pages[0];
    const lastPage = pages[pages.length - 1];

    // if we only have one page, and it's fetching, then it's the initial
    // page load so let the user know we're loading the data
    if (numberOfPages === 1 && lastPage.state === PayloadStates.FETCHING) {
      return (
        <div className="loader" />
      );
    }

    return (
      <div>
        <ul className="media-list tweets">
          {other ? other.data.map(row) : null}
          {_.flatten(pages.map((models) => {
            return _.filter(models.data, (model) => {
              return !exclude(model);
            }).map(row);
          }))}
        </ul>
        {selectNextPage ? (
          <LoadMoreButton
            lastPage={lastPage}
            onLoadMore={this.onLoadMore}
            nextPageMetaField="nextPage"
          />
        ) : null}
      </div>
    );
  }
}

export default InfiniteScrollingList;

Next Steps

Next we'll convert the Feed to use Infinite Scrolling.