Relay modern pagination using Refetch container

sushil bansal
Entria
Published in
5 min readJan 4, 2018

--

There is simple way to use Refetch container as Pagination container. It took me a while to get my head around pagination container and how it worked. Let’s start:

  1. Create a Refetch Container (this is my running code): very basic refetch container with additional arguments: orderBy and postId
export default createRefetchContainer(PostDetailsContainer, graphql`
fragment PostDetailsContainer_viewer on Viewer
@argumentDefinitions(count: { type: "Int" }, cursor: { type: "String"}, orderBy: { type: "String" }, postId: { type: "ID!" }) {
id
selectedPost (postId: $postId) {
_id
id
title
author {
userId
userName
}
comments (first: $count, after: $cursor, orderBy: $orderBy, postId: $postId)
@connection(key: "PostDetailsContainer_comments", filters: ["postId"]) {
edges {
cursor
node {
_id
id
...CommentItemContainer_comment
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`,
graphql`
query PostDetailsContainerRefetchQuery ($count: Int!, $cursor: String, $orderBy: String, $postId: ID!) {
viewer {
...PostDetailsContainer_viewer @arguments(count: $count, cursor: $cursor, $orderBy, postId: $postId)
}
}
`,
);

@connection: connection helps you identifying your particular container through key. There can be multiple containers which are updating the same set of data. If you do not have the connection you do not have the handle on your data in Relay Store. Check the example below provided in subscription. We will use the same connectionName while getting the ConnectionHandler in mutation/subscription.

const conn = ConnectionHandler.getConnection(postProxy, connectionName, filters);

filters: you need to add filters:[] in@connection to make it work. It is not very clear from the Relay documentation. It is always a good practice to use the right filter. Example: in my case i need to filter comments based on a Post Id which i passed as an argument to my connection. I will use the same post id to fetch the connection in my mutation and/or subscription as shown below:

const filters = {
postId: this.props.viewer.selectedPost._id,
};

2. CommentAddedSubscription: see the usage of filters when fetching connection. i pass filters like this to subscription:

const subscription = graphql`
subscription CommentAddedSubscription($input: ID!) {
commentAdded(postId: $input) {
id
_id
text
likes
dislikes
createdAt
author {
userId
userName
]
}
}
`;
const commit = (environment, connectionName, post, filters, onCompleted, onError) => requestSubscription(
environment,
{
subscription,
variables: {
input: post._id,
},
updater: (store) => {
const commentNode = store.getRootField('commentAdded');
const postProxy = store.get(post.id);
const conn = ConnectionHandler.getConnection(postProxy, connectionName, filters);
const commentEdge = ConnectionHandler.createEdge(store, conn, commentNode, 'comments');
ConnectionHandler.insertEdgeBefore(conn, commentEdge);
},
onCompleted,
onError,
}
);
export default { commit };

3. Initial number of rows to fetch:

you can pass the initial arguments through QueryRenderer like this:

<QueryRenderer
environment={environment}
query={graphql`
query PostDetailsScreenQuery($count: Int!, $cursor: String, $orderBy: String, $postId: ID!) {
viewer {
...PostDetailsContainer_viewer @arguments(count: $count, cursor: $cursor, $orderBy, postId: $postId)
}
}
`}
variables={{
count: 10,
cursor: null,
postId: this.props.navigation.state.params.selectedPostId,
orderBy: 'createdAt',
}}
render={this.renderScreen}
/>

4. Load more:

You need to be careful about certain type of variables and what these mean in both Refetch container and Pagination container.

fragmentVariables: are those variables which gets passed down from QR. So i will get orderBy, postId, count from QR.

Advanced usage only (please revisit once you have read the article till the end) — If you are passing variables dynamically i.e. at the time of using the container as part of larger container and not at the time of using in QR. It is an advanced usage and i have provided example at the end of this article.

refetchVariables: it will be used to fetch the data from DB. We initially fetched 10 record through QR. When user clicks on Load more then we need to fetch next 5 or 10 records (depending upon your count of load more). I am fetching next 5 records.

When i am loading more i am creating new refetchVariables from old refetchVariables. I am changing the count to 5 as i want to fetch next 5. If you want to fetch next 10 put 10 as count. I am fetching the endCursor from pageInfo. We want to fetch the next 5/10 records from the last fetched record. Initial value for endCursor was null which was passed down from QR. We have to change this value to reflect the last fetched record.

renderVariables: This set of variables will be used to fetch the data from Store to display at the front end. Initially we fetched 10 record through refetchVariables which was passed from QR. When user clicks on Load More button then we will fetch next 5 from DB. All this data gets stored in Relay Store. We need to fetch these 15 records from Store to display at the front end.

So we need to do const total = comments.edges.length + 5;

Let’s run through the following code. First i am checking if i am already loading records (i.e. any previous Load More request) then return. Second if there are no more comments then return.

We will fetch only next 5 comments from DB through refetchVariables and in total 15 comments from store through renderVariables. Hopefully it is clear now.

onLoadMore = () => {
const { loading } = this.state;
if (loading) return;

const { comments } = this.props.viewer.selectedPost;
if (!comments || !comments.pageInfo.hasNextPage) return;

this.setState({ loading: true });
const { endCursor } = comments.pageInfo;

// Fetch 5 more comments
const total = comments.edges.length + 5;
const refetchVariables = fragmentVariables => ({
...fragmentVariables,
count: 5,
cursor: endCursor,
});

const renderVariables = { count: total };
this.props.relay.refetch(
refetchVariables,
renderVariables,
() => {
this.setState({ loading: false });
}, {
force: false,
},
);
}

I hope it was not that complex. Please let me know in comments. I switched to this approach as i had interesting situation. Requirement was:

i have articles/posts which belongs to a specific category like politics, sports etc. I created one CategoryRowContainer which will fetch me posts based on one category (passed dynamically). Next i need to fetch multiple of CategoryRowContainer based on each category. List of categories were stored in DB. Refetch container helped me a lot as following:

  1. I passed initial category as empty and initial number of posts to zero to load in QR.
  2. When container was loaded i refetched the posts with the right category and right number of posts (10 initially).
  3. Load more was easy as shown above.

You can see that i have used only container (CategoryRowContainer) to fetch posts for a category and the same container for array of categories.

// First render List of all Categories as FlatList as rows
// Then render each element of cateory as FlatList of posts
renderCategoryItemList = (viewer) => (
<FlatList
data={CATEGORIES}
renderItem={({ item }) => this.renderItem(viewer, item)}
keyExtractor={this.keyExtractor}
onEndReachedThreshold={0}
ListFooterComponent={this.renderFooter}
/>
);
// CategoryItemRowContainer has another FlatList to display posts in a row
renderItem
= (viewer, item) => (
<CategoryItemRowContainer
viewer={viewer}
orderBy="createdAt"
category={item}
key={item}
/>
);

And my QR:

<QueryRenderer
environment={environment}
query={graphql`
query HomeScreenQuery($count: Int!, $cursor: String, $orderBy: String, $category: String!) {
viewer {
...CategoryItemRowContainer_viewer @arguments(count: $count, cursor: $cursor, orderBy: $orderBy, category: $category)
}
}
`}
variables={{
count: 0,
cursor: null,
orderBy: '',
category: '',
}}
render={this.renderScreen}
/>

Hope it helped. I would like to thank Sibelius (Sibelius Seraphini) who provided his time and helped me in debugging my code on numerous occasions. I used one of his loader packages (graphql-mongoose-loader) if anyone wants to check out how to use loader with graphql and mongoose.

--

--