Relay: [Modern] Paginating a connection in the root query

Created on 28 Apr 2017  Â·  9Comments  Â·  Source: facebook/relay

In my app I have a component called BlogFeed that expects an array of BlogPostEdge objects to be passed in via its posts prop. It looks something like this:

const BlogFeed = ({ posts, title }) => (
  <div>
    <h1>{title}</h1>
    {posts.edges.map(edge => (
      <BlogPostFragmentContainer key={edge.cursor} post={edge.node} />
    ))}
  </div>
)

Right now I'm wrapping that in a fragment container:

export default createFragmentContainer(
  BlogFeed,
  graphql`
    fragment BlogFeedFragmentContainer_posts on BlogPostConnection {
      edges {
        cursor
        node {
          ...BlogPostFragmentContainer_post
        }
      }
    }
  `
)

And then finally populating that with the following root GraphQL query

query MainBlogFeedQuery($first: Int!, $cursor: String) {
  blogPosts(first:$first, after:$cursor) {
    ...BlogFeedFragmentContainer_posts
  }
}

I'd like to change this component's container to a PaginationContainer, but I've run into a few issues doing so. The root issue is that to use the @connection directive on my root query, I have to include an edges selection, or else I get an error from the Relay Compiler:

RelayConnectionTransform: Expected field `blogPosts: BlogPostConnection` to have a edges selection in document

But since I have an edges selection on my container's fragment, I can't do that. Including multiple edges selections results in my server only respecting the first one requested.

Is there an obvious solution I'm missing? Or failing that, some kind of workaround?

Most helpful comment

@ajchambeaud sure thing! The pagination container essentially looks like this:

export default createPaginationContainer(
  BlogFeed,
  graphql`
    fragment PaginatedBlogFeedContainer_feed on RootQueryType {
      blogPosts(
        first: $count,
        after: $cursor
      ) @connection(key: "PaginatedBlogFeedContainer_blogPosts") {
        edges {
          cursor
          node {
            ...BlogPostSummaryContainer_post
          }
        }
      }
    }
  `,
  {
    query: graphql`
      query PaginatedBlogFeedContainerQuery(
        $count: Int!
        $cursor: String
      ) {
        ...PaginatedBlogFeedContainer_feed
      }
    `,
    getFragmentVariables(prevVars, totalCount) {
      return {
        ...prevVars,
        count: totalCount
      }
    },
    getVariables(props, { count, cursor }, fragmentVariables) {
      return {
        count,
        cursor
      }
    }
  }
)

and then the root query container looks like this:

const MainBlogFeed = ({ location }) => {

  const query: any = graphql`
    query MainBlogFeedQuery($count: Int!, $cursor: String) {
      ...PaginatedBlogFeedContainer_feed
    }
  `

  const variables = {
    count: 5,
    cursor: null
  }

  const render = ({ error, props, retry }) => {
    if (error) {
      return <div>{error.message}</div>
    } else if (props) {
      return <PaginatedBlogFeedContainer title="All Posts" feed={props} />
    } else {
      return <p>Loading…</p>
    }
  }

  return (
    <QueryRenderer
      environment={environment}
      query={query}
      variables={variables}
      render={render}
    />
  )
}

All 9 comments

Currently @connection requires that the edges field be a direct child; using a connection where the only selection is a fragment spread isn't supported.

I would try moving the blogPosts field to the pagination container.

@josephsavona thanks! That totally fixed it for me

@rpowelll can you show your solution? I'm having the same issues.

@ajchambeaud sure thing! The pagination container essentially looks like this:

export default createPaginationContainer(
  BlogFeed,
  graphql`
    fragment PaginatedBlogFeedContainer_feed on RootQueryType {
      blogPosts(
        first: $count,
        after: $cursor
      ) @connection(key: "PaginatedBlogFeedContainer_blogPosts") {
        edges {
          cursor
          node {
            ...BlogPostSummaryContainer_post
          }
        }
      }
    }
  `,
  {
    query: graphql`
      query PaginatedBlogFeedContainerQuery(
        $count: Int!
        $cursor: String
      ) {
        ...PaginatedBlogFeedContainer_feed
      }
    `,
    getFragmentVariables(prevVars, totalCount) {
      return {
        ...prevVars,
        count: totalCount
      }
    },
    getVariables(props, { count, cursor }, fragmentVariables) {
      return {
        count,
        cursor
      }
    }
  }
)

and then the root query container looks like this:

const MainBlogFeed = ({ location }) => {

  const query: any = graphql`
    query MainBlogFeedQuery($count: Int!, $cursor: String) {
      ...PaginatedBlogFeedContainer_feed
    }
  `

  const variables = {
    count: 5,
    cursor: null
  }

  const render = ({ error, props, retry }) => {
    if (error) {
      return <div>{error.message}</div>
    } else if (props) {
      return <PaginatedBlogFeedContainer title="All Posts" feed={props} />
    } else {
      return <p>Loading…</p>
    }
  }

  return (
    <QueryRenderer
      environment={environment}
      query={query}
      variables={variables}
      render={render}
    />
  )
}

@rpowelll zzz this was so hard to find. Why is this not mentioned on the Pagination page?!? How are we supposed to know that @connection requires that the edges field be a direct child...

Thanks.

@rpowelll Thanks for sharing your code snippets.
I have followed your snippets but got the error below

_Fragment "Products" cannot be spread here as objects of type "ProductConnection" can never be of type "Query"._

Looks like fragment Products can not be on Query which is root.

Any idea?

My relay version is 1.4.1 and see my code for reference.

index.js

import React, { Component } from 'react';
import { QueryRenderer, graphql } from 'react-relay';
import environment from 'Environment';

import Products from 'components/Main/Products';

const productsQuery = graphql`
  query ProductsContainerQuery($count: Int!, $cursor: String) {
    products {
      ...Products
    }
  }
`;

const variables = {
  count: 10,
  cursor: null
};

export default class ProductsContainer extends Component {
  render() {
    return (
      <QueryRenderer
        environment={environment}
        query={productsQuery}
        variables={{
          count: 10,
          cursor: null
        }}
        render={({ error, props }) => {
          if (error) {
            return <div>{error.message}</div>;
          } else if (props) {
            return <Products {...props} />;
          }
          return <div>Loading</div>;
        }}
      />
    );
  }
}
import React, { Component } from 'react';
import { createPaginationContainer, graphql } from 'react-relay';
import { withRouter } from 'react-router-dom';

import { Container } from 'semantic-ui-react';

import Header from './Header';
import ProductsContainer from 'containers/Main/Products/Products';

class Products extends Component {
  addProduct = e => this.props.history.push('/products/new');

  render() {
    const products = this.props.products.edges.map(x => ({
      id: x.node.id,
      price: x.node.price,
      name: x.node.parrot.name,
      supplier: x.node.supplier.name
    }));

    return (
      <Container>
        <Header addProduct={this.addProduct} />
        <Container>
          <ProductsContainer {...{ products }} />
        </Container>
      </Container>
    );
  }
}

export default createPaginationContainer(
  withRouter(Products),
  graphql`
    fragment Products on Query {
      products(first: $count, after: $cursor)
        @connection(key: "Products_products", filters: []) {
        edges {
          cursor
          node {
            id
            price
            parrot {
              id
              name
              description
            }
            supplier {
              name
            }
          }
        }
      }
    }
  `,
  {
    query: graphql`
      query ProductsQuery($count: Int!, $cursor: String) {
        ...Products
      }
    `,
    getFragmentVariables(prevVars, totalCount) {
      return {
        ...prevVars,
        count: totalCount
      };
    },
    getVariables(props, { count, cursor }, fragmentVariables) {
      return {
        count,
        cursor
      };
    }
  }
);

schema.graphql

...
type Query {
  product(id: ID!): Product
  products(skip: Int, after: String, before: String, first: Int, last: Int, supplierId: ID): ProductConnection
}
...

@DavidHe1127 the problem is here:

query: graphql`
      query ProductsQuery($count: Int!, $cursor: String) {
        ...Products
      }
    `,

Instead, try this:

query: graphql`
      query ProductsQuery($count: Int!, $cursor: String) {
        products(first: $count, after: $cursor) {
          ...Products
        }
      }
    `,

@jgcmarins
The bottom of the issue is in index.js

const productsQuery = graphql`
  query ProductsContainerQuery($count: Int!, $cursor: String) {
    products {
      ...Products
    }
  }
`;

needs to be changed to

const productsQuery = graphql`
  query ProductsContainerQuery($count: Int!, $cursor: String) {
    ...Products
  }
`;

But thanks for your comments anyway

+1 Can't use @connection in fragment, cannot use createPaginationContainer without @connection.

Was this page helpful?
0 / 5 - 0 ratings