Features

Key features that make up the main value proposition for Lore

Pagination

Useful for moving through large datasets, and providing the user with a sliced view of the data.

Visualization

This video demonstrates what pagination looks like. Screenshots are from the Simply Social prototype that Invision provides you when you sign up for an account.

Usage

Let's say you have an API with a /posts endpoint, and you want to create an experience that lets a user browse through all the Posts. But there are a lot of posts, and they can't all be retrieved in a single request. Instead, you're going to need to retrieve the first page of results, and then fetch additional pages when the user wants to view them.

So let's start by retrieving the first page of results. To do that you're going to pass a pagination clause to theconnect call like this:

connect(function(getState, props) {
  return {
    posts: getState('post.find', {
      pagination: {
        page: 1
      }
    })
  };
})

Assuming your API server is located at https://api.example.com, this call will get transformed into the following network request:

https://api.example.com/posts?page=1

Lore's default behavior is to convert the pagination clause into a query string. So if you passed in a second property to the where clause, like this:

pagination: {
  page: 1,
  pageSize: 20
}

It would get converted into this network request:

https://api.example.com/posts?page=1&pageSize=20

Side Note

You can also combine a where clause with pagination in order to paginate through the result of a query. To do that, create a connect call that looks like this:

connect(function(getState, props) {
  return {
    posts: getState('post.find', {
      where: {
        authorId: '123'
      },
      pagination: {
        page: 1
      }
    })
  };
})

In this case, the final query parameters will be composed of both the where parameters and the pagination parameters, creating a network request that looks like this:

https://api.example.com/posts?authorId=123&page=1

Since it will take time for the network request to complete, let's focus on the component next. The component is going to receive a prop named posts containing the result of our request for data.

@connect(function(getState, props) {
  return {
    posts: getState('post.find', {
      pagination: {
        page: 1
      }
    })
  };
})
class Component extends React.Component {

  static propTypes = {
    posts: React.PropTypes.object.isRequired
  };

  render() {
    var posts = this.props.posts;

    // todo: render the posts
  }
};

Since our network request was just sent out, we won't have any data yet. So posts will look like this:

posts = {
  state: 'FETCHING',
  data: [],
  query: { pagination: { page: 1 } }
}

We don't have any data to render yet, but the query field tells us what data we asked for, and the state field tells us that data is being fetched. So let's render a loading message while we're waiting for the data to return from the API server.

@connect(function(getState, props) {
  return {
    posts: getState('post.find', {
      where: {
        authorId: props.params.authorId
      }
    })
  };
})
class Component extends React.Component {

  static propTypes = {
    posts: React.PropTypes.object.isRequired
  };

  render() {
    var posts = this.props.posts;

    if (posts.state === PayloadStates.FETCHING) {
      return (
        <div>Loading posts...</div>
      );
    }

    // todo: render posts
  }
};

After a time our network request will come back, the Redux store will be updated, and our component will be rerendered. But this time we'll actually have data, so our posts will look like this:

posts = {
  state: 'RESOLVED',
  data: [
    {
      id: '1',
      cid: 'c1',
      state: 'RESOLVED',
      data: {
        title: 'Bacon is yummy',
        authorId: '123'
      }
    },
    {
      id: '2',
      cid: 'c1',
      state: 'RESOLVED',
      data: {
        title: 'An ode to Bacon',
        authorId: '123'
      }
    }
  ],
  query: { pagination: { page: 1 } }
}

The state value of RESOLVED let's you know that nothing is happening; the request has been resolved. And now, ourdata property contains an array of Posts. So let's render that array to display the first page of posts:

connect(function(getState, props) {
    return {
      posts: getState('post.find', {
        where: {
          authorId: props.params.authorId
        }
      })
    };
  })(
  createReactClass({

    propTypes: {
      posts: React.PropTypes.object.isRequired
    },

    render: function() {
      var posts = this.props.posts;

      if (posts.state === PayloadStates.FETCHING) {
        return (
          <div>Loading posts...</div>
        );
      }

      return (
        <div>
          <ul>
            {posts.data.map((post) => {
              return (
                <li key={post.id || post.cid}>
                  {post.data.title}
                </li>
              );
            })}
          </ul>
          { /* todo: render pagination links */}
        </div>
      );
    }
  })
);

Next we need to render a way for the user to change which page of data they're looking at. For that, we're going to take advantage of react-router and render a couple navigation links below our list of posts:

connect(function(getState, props) {
    return {
      posts: getState('post.find', {
        where: {
          authorId: props.params.authorId
        }
      })
    };
  })(
  createReactClass({

    propTypes: {
      posts: React.PropTypes.object.isRequired
    },

    render: function() {
      var posts = this.props.posts;

      if (posts.state === PayloadStates.FETCHING) {
        return (
          <div>Loading posts...</div>
        );
      }

      return (
        <div>
          <ul>
            {posts.data.map((post) => {
              return (
                <li key={post.id || post.cid}>
                  {post.data.title}
                </li>
              );
            })}
          </ul>
          <div>
            <Link to={{ pathname: '/posts', query: { page: 1 } }} />
            <Link to={{ pathname: '/posts', query: { page: 2 } }} />
          </div>
        </div>
      );
    }
  })
);

Whenever the user clicks on these links, the browser URL will change and resemble something like this:

https://www.myapp.com/posts?page=2

And whenever the browser URL changes, our component will be rerendered, and will get the chance to request a new page of posts. However, right now the page of posts we're requesting is hard-coded to the first page. So let's change ourconnect call to get the page from the query parameter instead so that the data we request will match the application URL:

connect(function(getState, props) {
  return {
    posts: getState('post.find', {
      pagination: {
        page: props.params.page
      }
    })
  };
})

And now that the page of posts is being driven off the query parameter, all we need to do is change the browser URL and our data will automatically be updated.

Now this example is still a little too simple still. In a real application, we won't want to hard-code the pagination links. Instead you would want to calculate the number of pages based on the total number of Posts in the API (something the API should tell you in the result set) divided by the number of Posts in each page. For example, if the API told you there were 102 Posts, and you were retrieving 20 posts per page of data, then you tell the user there are 6 pages of data. For an example of how to do this, see the pagination exampleon GitHub.

Example Code

Example code showing how to paginate data.

connect(function(getState, props) {
    return {
      posts: getState('post.find', {
        where: {
          authorId: props.params.authorId
        },
        pagination: {
          page: props.location.query.page
        }
      })
    };
  })(
  createReactClass({

    propTypes: {
      posts: React.PropTypes.object.isRequired
    },

    renderPost: function(post) {
      return (
        <li key={post.id || post.cid}>
          {post.data.title}
        </li>
      );
    },

    render: function() {
      var posts = this.props.posts;

      if (posts.state === PayloadStates.FETCHING) {
        return (
          <div>Loading posts...</div>
        );
      }

      return (
        <ul>
          {posts.data.map(this.renderPost)}
        </ul>
      );
    }
  })
);
class Component extends React.Component {

  constructor(props) {
    super(props);
    this.renderPost = this.renderPost.bind(this);
  }

  renderPost(post) {
    return (
      <li key={post.id || post.cid}>
        {post.data.title}
      </li>
    );
  }

  render() {
    var posts = this.props.posts;

    if (posts.state === PayloadStates.FETCHING) {
      return (
        <div>Loading posts...</div>
      );
    }

    return (
      <ul>
        {posts.data.map(this.renderPost)}
      </ul>
    );
  }
};

Component.PropTypes = {
  posts: React.PropTypes.object.isRequired
};

export default connect(function(getState, props) {
  return {
    posts: getState('post.find', {
      where: {
        authorId: props.params.authorId
      },
      pagination: {
        page: props.location.query.page
      }
    })
  };
})(Component);
@connect(function(getState, props) {
  return {
    posts: getState('post.find', {
      where: {
        authorId: props.params.authorId
      },
      pagination: {
        page: props.location.query.page
      }
    })
  };
})
class Component extends React.Component {

  static propTypes = {
    posts: React.PropTypes.object.isRequired
  };

  constructor(props) {
    super(props);
    this.renderPost = this.renderPost.bind(this);
  }

  renderPost(post) {
    return (
      <li key={post.id || post.cid}>
        {post.data.title}
      </li>
    );
  }

  render() {
    var posts = this.props.posts;

    if (posts.state === PayloadStates.FETCHING) {
      return (
        <div>Loading posts...</div>
      );
    }

    return (
      <ul>
        {posts.data.map(this.renderPost)}
      </ul>
    );
  }
};