paint-brush
Bi-Directional Cursor Pagination with React.js, Relay, and GraphQLby@meeq
3,964 reads
3,964 reads

Bi-Directional Cursor Pagination with React.js, Relay, and GraphQL

by Christopher BonhageMay 26th, 2017
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Pagination comes in many different flavors depending on the desired user experience and the shape of the underlying API. <a href="https://hackernoon.com/tagged/graphql" target="_blank">GraphQL</a> APIs such as <a href="https://developer.github.com/early-access/graphql/" target="_blank">GitHub</a>’s implement the <a href="https://facebook.github.io/relay/graphql/connections.htm" target="_blank">Relay Cursor Connections Specification</a> to standardize pagination and slicing of large result sets. This approach is well-suited to infinite-scrolling, but can also be used for “windowed” paging with next/previous page buttons.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Bi-Directional Cursor Pagination with React.js, Relay, and GraphQL
Christopher Bonhage HackerNoon profile picture

Pagination comes in many different flavors depending on the desired user experience and the shape of the underlying API. GraphQL APIs such as GitHub’s implement the Relay Cursor Connections Specification to standardize pagination and slicing of large result sets. This approach is well-suited to infinite-scrolling, but can also be used for “windowed” paging with next/previous page buttons.

Cursor connections work by passing in one of the following query argument pairs:

Forward Windowed Pagination

first is a positive, non-zero integer describing the maximum number of results to return from the leading side of the results set. During backward-pagination, this value must be null.

after is an opaque cursor type value provided by the endCursor field of the connection’s [pageInfo](https://developer.github.com/early-access/graphql/object/pageinfo/) object. For the first page, this value will be null. During backward-pagination, this value must be null.

Backward Windowed Pagination

last is a positive, non-zero integer describing the maximum number of results to return from the trailing side of the results set. During forward-pagination, this value must be null.

before is an opaque cursor type value provided by the startCursor field of the connection’s [pageInfo](https://developer.github.com/early-access/graphql/object/pageinfo/) object. During forward-pagination, this value must be null.

GraphQL Query fragment example






















Relay.QL`fragment on Query {search(query: $q, type: REPOSITORY,first: $first, after: $after,last: $last, before: $before) {repositoryCountpageInfo {startCursorendCursor}edges {node {... on Repository {idnameurl}}}}}`

The repositoryCount field is used to calculate the total number of result pages. [pageInfo](https://developer.github.com/early-access/graphql/object/pageinfo/) is used to populate the before/after cursor arguments. [edges](https://developer.github.com/early-access/graphql/object/searchresultitemedge) contains the search results inside the [node](https://developer.github.com/early-access/graphql/union/searchresultitem/) Union type. We have restricted the results to only contain [repository](https://developer.github.com/early-access/graphql/object/repository/) type nodes using the type argument.

Using the data from this query, the application can calculate the total number of pages and keep track of which page we are currently on. The page count is the rounded-up result of dividing the total number of results by the number of results per page.

To see the next page, run the query using the endCursor field of the current page as the after argument and the page size as the first argument. To go back a page, run the query using the startCursor field of the current page as the before argument and the page size as the last argument.

Encapsulating pagination logic in a React Component

class Search extends Component {

In order to propagate the query results through a Relay Container, we need to create a React Component to process, act on, and display the data.


constructor(props) {super(props);

this.state = {  
  q: "",  
  pageSize: 25,  
  pageCursor: 1,  
  pageCount: 1  
};

this.handleQueryChange = this.handleQueryChange.bind(this);  
this.handleSearch = this.handleSearch.bind(this);  
this.handlePrevPage = this.handlePrevPage.bind(this);  
this.handleNextPage = this.handleNextPage.bind(this);  

}

The constructor is mostly boilerplate with some initial state and function bindings to ensure the component is the function context during callbacks.





handleQueryChange(e) {this.setState({q: e.target.value});}

handleQueryChange is called by an <input> onChange callback to update the search query based on user input. The actual search does not happen until the form is submitted.











handleSearch(e) {this.props.relay.setVariables({q: this.state.q,first: this.state.pageSize,after: null,last: null,before: null});this.isNewSearchQuery = true;e && e.preventDefault(); // Stop form action}

handleSearch is called by the <form> onSubmit callback to perform the search by updating the variables in the Relay query. The isNewSearchQuery flag is used by componentWillReceiveProps as a cue to reset the page number and page count when the query value changes.











componentWillReceiveProps(nextProps) {if (this.isNewSearchQuery) {let repoCount = nextProps.query.search.repositoryCount;let reposPerPage = this.state.pageSizethis.setState({pageCursor: 1,pageCount: Math.ceil(repoCount / reposPerPage)});this.isNewSearchQuery = false;}}

[componentWillReceiveProps](https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops) is a React Component hook that is called when the query results are returned. From here, we can check if the isNewSearchQuery flag was set and reset the page cursor to the beginning and re-calculate the page count. This pattern works best when the total number of results does not wildly change during pagination.















handlePrevPage() {if (this.state.pageCursor > 1) {let pageCursor = this.state.pageCursor - 1;this.props.relay.setVariables({last: this.state.pageSize,before: this.props.query.search.pageInfo.startCursor,first: null,after: null}, ({ ready, done }) => {if (ready && done) {this.setState({ pageCursor });}});}}















handleNextPage() {if (this.state.pageCursor < this.state.pageCount) {let pageCursor = this.state.pageCursor + 1;this.props.relay.setVariables({first: this.state.pageSize,after: this.props.query.search.pageInfo.endCursor,last: null,before: null}, ({ ready, done }) => {if (ready && done) {this.setState({ pageCursor });}});}}

handlePrevPage and handleNextPage perform bounds checking based on the pagination state, then update the Relay query variables using the current results’ [pageInfo](https://developer.github.com/early-access/graphql/object/pageinfo/). The actual page number does not update until the new query has completed by utilizing the optional onReadyStateChange argument of [setVariables](https://facebook.github.io/relay/docs/api-reference-relay-container.html#setvariables).








render() {let { search } = this.props.query;let { pageCursor, pageCount } = this.state;let isPrevPageDisabled = (pageCursor === 1);let isNextPageDisabled = (pageCursor === pageCount);return (<div><h2>Search</h2>

    <form onSubmit={this.handleSearch}>  
      <input placeholder="Repository name" type="text" value={this.state.q} onChange={this.handleQueryChange} />  
      &nbsp;  
      <button type="submit">Search</button>  
    </form>

    <h3>{search.repositoryCount} results</h3>

    <button onClick={this.handlePrevPage} disabled={isPrevPageDisabled}>Previous Page</button>  
    &nbsp;  
    Page {pageCursor} of {pageCount}  
    &nbsp;  
    <button onClick={this.handleNextPage} disabled={isNextPageDisabled}>Next Page</button>

    <ul className="Search-results">  
      {search.edges.map(({ node }, index) => (  
        <SearchResult key={node.id} repository={node} />  
      ))}  
    </ul>  
  </div>  
);  


}}

render creates the DOM for the Search component, hooking up all of the callbacks and displaying the results by mapping through the [edges](https://developer.github.com/early-access/graphql/object/searchresultitemedge) and encapsulating the search result items in their own component:













class SearchResult extends Component {render() {let repo = this.props.repository;let stargazersUrl = repo.url + "/stargazers";return (<li><h5 className="stargazers"><a href={stargazersUrl}>★ {repo.stargazers.totalCount}</a></h5><h4 className="headline"><a href={repo.owner.url}>{repo.owner.login}</a>/<a href={repo.url}>{repo.name}</a></h4><span className="description">{repo.description}</span></li>)}}

Wrapping it all up with a Relay Container

Finally, we can create a Relay Container based on our query fragments and React Component:









































Relay.createContainer(Search, {initialVariables: {q: "",first: null,last: null,before: null,after: null},fragments: {query: () => Relay.QL`fragment on Query {search(query: $q, type: REPOSITORY,first: $first, after: $after,last: $last, before: $before) {repositoryCountpageInfo {startCursorendCursor}edges {node {... on Repository {idnameurldescriptionstargazers {totalCount}owner {loginurl}}}}}}`,},});

This container can be passed into a Relay Renderer or used as a child-container inside of a parent with the [getFragment](https://facebook.github.io/relay/docs/api-reference-relay-container.html#getfragment) static method.

I hope this was helpful for beginners trying to wrap their head around the GraphQL pagination model and integrate with React and Relay Classic. I wanted to share this because I could not find any examples of bi-directional pagination in GraphQL while solving this exercise.

Thanks for reading. If you are looking for an experienced JavaScript engineer, send me a message on LinkedIn or drop me an email at jobs at christopherbonhage.com