Claude
Skills
Sign in
Back

relay-pagination

Included with Lifetime
$97 forever

Use when relay pagination with cursor-based pagination, infinite scroll, load more patterns, and connection protocols.

General

What this skill does


# Relay Pagination

Master Relay's cursor-based pagination for efficiently loading and displaying
large datasets with infinite scroll and load more patterns.

## Overview

Relay implements the GraphQL Cursor Connections Specification for efficient
pagination. It provides hooks like usePaginationFragment for declarative
pagination with automatic cache updates and connection management.

## Installation and Setup

### Pagination Query Structure

```graphql
# schema.graphql
type Query {
  posts(
    first: Int
    after: String
    last: Int
    before: String
  ): PostConnection!
}

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int
}

type PostEdge {
  cursor: String!
  node: Post!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type Post {
  id: ID!
  title: String!
  body: String!
}
```

## Core Patterns

### 1. Basic Pagination

```javascript
// PostsList.jsx
import { graphql, usePaginationFragment } from 'react-relay';

const PostsListFragment = graphql`
  fragment PostsList_query on Query
  @refetchable(queryName: "PostsListPaginationQuery")
  @argumentDefinitions(
    first: { type: "Int", defaultValue: 10 }
    after: { type: "String" }
  ) {
    posts(first: $first, after: $after)
    @connection(key: "PostsList_posts") {
      edges {
        node {
          id
          ...PostCard_post
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`;

function PostsList({ query }) {
  const {
    data,
    loadNext,
    loadPrevious,
    hasNext,
    hasPrevious,
    isLoadingNext,
    isLoadingPrevious,
    refetch
  } = usePaginationFragment(PostsListFragment, query);

  return (
    <div>
      <button
        onClick={() => refetch({ first: 10 })}
        disabled={isLoadingNext}
      >
        Refresh
      </button>

      {data.posts.edges.map(({ node }) => (
        <PostCard key={node.id} post={node} />
      ))}

      {hasNext && (
        <button
          onClick={() => loadNext(10)}
          disabled={isLoadingNext}
        >
          {isLoadingNext ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}

export default PostsList;
```

### 2. Infinite Scroll

```javascript
// InfiniteScrollPosts.jsx
import { useEffect, useRef } from 'react';
import { graphql, usePaginationFragment } from 'react-relay';

const InfiniteScrollFragment = graphql`
  fragment InfiniteScrollPosts_query on Query
  @refetchable(queryName: "InfiniteScrollPostsQuery")
  @argumentDefinitions(
    first: { type: "Int", defaultValue: 20 }
    after: { type: "String" }
  ) {
    posts(first: $first, after: $after)
    @connection(key: "InfiniteScroll_posts") {
      edges {
        node {
          id
          ...PostCard_post
        }
      }
    }
  }
`;

function InfiniteScrollPosts({ query }) {
  const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment(
    InfiniteScrollFragment,
    query
  );

  const observerRef = useRef();
  const loadMoreRef = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNext && !isLoadingNext) {
          loadNext(20);
        }
      },
      { threshold: 0.5 }
    );

    const currentRef = loadMoreRef.current;
    if (currentRef) {
      observer.observe(currentRef);
    }

    observerRef.current = observer;

    return () => {
      if (currentRef) {
        observer.unobserve(currentRef);
      }
    };
  }, [hasNext, isLoadingNext, loadNext]);

  return (
    <div>
      {data.posts.edges.map(({ node }) => (
        <PostCard key={node.id} post={node} />
      ))}

      {hasNext && (
        <div ref={loadMoreRef} className="load-more-trigger">
          {isLoadingNext && <Spinner />}
        </div>
      )}

      {!hasNext && <div>No more posts</div>}
    </div>
  );
}
```

### 3. Bidirectional Pagination

```javascript
// BidirectionalPosts.jsx
const BidirectionalFragment = graphql`
  fragment BidirectionalPosts_query on Query
  @refetchable(queryName: "BidirectionalPostsQuery")
  @argumentDefinitions(
    first: { type: "Int" }
    after: { type: "String" }
    last: { type: "Int" }
    before: { type: "String" }
  ) {
    posts(first: $first, after: $after, last: $last, before: $before)
    @connection(key: "Bidirectional_posts") {
      edges {
        node {
          id
          ...PostCard_post
        }
      }
      pageInfo {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }
    }
  }
`;

function BidirectionalPosts({ query }) {
  const {
    data,
    loadNext,
    loadPrevious,
    hasNext,
    hasPrevious,
    isLoadingNext,
    isLoadingPrevious
  } = usePaginationFragment(BidirectionalFragment, query);

  return (
    <div>
      {hasPrevious && (
        <button
          onClick={() => loadPrevious(10)}
          disabled={isLoadingPrevious}
        >
          {isLoadingPrevious ? 'Loading...' : 'Load Previous'}
        </button>
      )}

      {data.posts.edges.map(({ node }) => (
        <PostCard key={node.id} post={node} />
      ))}

      {hasNext && (
        <button
          onClick={() => loadNext(10)}
          disabled={isLoadingNext}
        >
          {isLoadingNext ? 'Loading...' : 'Load Next'}
        </button>
      )}
    </div>
  );
}
```

### 4. Filtered Pagination

```javascript
// FilteredPosts.jsx
const FilteredPostsFragment = graphql`
  fragment FilteredPosts_query on Query
  @refetchable(queryName: "FilteredPostsQuery")
  @argumentDefinitions(
    first: { type: "Int", defaultValue: 10 }
    after: { type: "String" }
    status: { type: "PostStatus" }
    authorId: { type: "ID" }
  ) {
    posts(
      first: $first
      after: $after
      status: $status
      authorId: $authorId
    )
    @connection(key: "FilteredPosts_posts") {
      edges {
        node {
          id
          ...PostCard_post
        }
      }
    }
  }
`;

function FilteredPosts({ query }) {
  const [status, setStatus] = useState('PUBLISHED');
  const [authorId, setAuthorId] = useState(null);

  const { data, loadNext, hasNext, refetch } = usePaginationFragment(
    FilteredPostsFragment,
    query
  );

  const handleFilterChange = (newStatus, newAuthorId) => {
    setStatus(newStatus);
    setAuthorId(newAuthorId);

    refetch({
      first: 10,
      after: null,
      status: newStatus,
      authorId: newAuthorId
    });
  };

  return (
    <div>
      <FilterControls
        status={status}
        authorId={authorId}
        onChange={handleFilterChange}
      />

      {data.posts.edges.map(({ node }) => (
        <PostCard key={node.id} post={node} />
      ))}

      {hasNext && (
        <button onClick={() => loadNext(10)}>Load More</button>
      )}
    </div>
  );
}
```

### 5. Pagination with Search

```javascript
// SearchablePosts.jsx
const SearchablePostsFragment = graphql`
  fragment SearchablePosts_query on Query
  @refetchable(queryName: "SearchablePostsQuery")
  @argumentDefinitions(
    first: { type: "Int", defaultValue: 10 }
    after: { type: "String" }
    searchTerm: { type: "String" }
  ) {
    posts(first: $first, after: $after, searchTerm: $searchTerm)
    @connection(key: "SearchablePosts_posts") {
      edges {
        node {
          id
          ...PostCard_post
        }
      }
      totalCount
    }
  }
`;

function SearchablePosts({ query }) {
  const [searchTerm, setSearchTerm] = useState('');
  const { data, loadNext, hasNext, refetch, isLoadingNext } =
    usePaginationFragment(SearchablePostsFragment, query);

  const handleSearch = (term) => {
    setSearchTerm(term);
    refetch({
      first: 10,
      after: null,
      searchTerm: term
    });
  };

  return (
    <div>
      <SearchInput
        value={searchTerm}
        onChange={handleSearch}
        placeholder="Search posts..."
      />

      <div>
        Showing {data.posts.edges.lengt

Related in General