Skip to main content
Solid Relay

Pagination

Relay provides powerful pagination features that work seamlessly with GraphQL connections. This guide covers how to implement efficient pagination using Solid Relay.

Basic Pagination with createPaginationFragment

Use createPaginationFragment to implement pagination with the connection pattern:

import { createPaginationFragment } from 'solid-relay';
import { graphql } from 'relay-runtime';
function UserList(props: { $query: UserList_query$key }) {
const query = createPaginationFragment(
graphql`
fragment UserList_query on Query
@argumentDefinitions(
count: { type: "Int!", defaultValue: 10 },
cursor: { type: "String" },
)
@refetchable(queryName: "UserListPaginationQuery") {
users(
first: $count,
after: $cursor,
) @connection(key: "UserList_users") {
edges {
node {
id
name
email
avatar
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`,
() => props.$query,
);
const handleLoadMore = () => {
if (query.hasNext && !query.isLoadingNext) {
query.loadNext(10); // Load 10 more items
}
};
const handleLoadPrevious = () => {
if (query.hasPrevious && !query.isLoadingPrevious) {
query.loadPrevious(10); // Load 10 previous items
}
};
return (
<div>
<Show when={query.hasPrevious}>
<button
onClick={handleLoadPrevious}
disabled={query.isLoadingPrevious}
>
{query.isLoadingPrevious ? 'Loading...' : 'Load Previous'}
</button>
</Show>
<div>
<For each={query()?.users.edges}>
{(edge) => (
<div>
<img src={edge.node.avatar} alt={edge.node.name} />
<h3>{edge.node.name}</h3>
<p>{edge.node.email}</p>
</div>
)}
</For>
</div>
<Show when={query.hasNext}>
<button
onClick={handleLoadMore}
disabled={query.isLoadingNext}
>
{query.isLoadingNext ? 'Loading...' : 'Load More'}
</button>
</Show>
</div>
);
}

Setting Up the Parent Query

The parent query needs to define the pagination variables:

const UserListPageQuery = graphql`
query UserListPageQuery($count: Int!, $cursor: String) {
...UserList_query
}
`;
function UserListPage() {
const data = createLazyLoadQuery(UserListPageQuery, {
count: 20, // Initial page size
cursor: null // Start from the beginning
});
return (
<Show when={data()}>
{(data) => (
<div>
<h1>All Users</h1>
<UserList $query={data()} />
</div>
)}
</Show>
);
}

Bidirectional Pagination

Support both forward and backward pagination:

function PostList(props: { $query: PostList_query$key }) {
const query = createPaginationFragment(
graphql`
fragment PostList_query on Query
@argumentDefinitions(
first: { type: "Int", defaultValue: 10 },
after: { type: "String" },
last: { type: "Int" },
before: { type: "String" },
)
@refetchable(queryName: "PostListPaginationQuery") {
posts(
first: $first,
after: $after,
last: $last,
before: $before,
) @connection(key: "PostList_posts") {
edges {
node {
id
title
excerpt
createdAt
author {
name
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`,
() => props.$query,
);
return (
<div>
<div>
<button
onClick={() => query.loadPrevious(5)}
disabled={!query.hasPrevious || query.isLoadingPrevious}
>
← Previous
</button>
<span>
{query.isLoadingNext || query.isLoadingPrevious
? 'Loading...'
: `${query()?.posts.edges.length || 0} posts`
}
</span>
<button
onClick={() => query.loadNext(5)}
disabled={!query.hasNext || query.isLoadingNext}
>
Next →
</button>
</div>
<div>
<For each={query()?.posts.edges}>
{(edge) => (
<article>
<h2>{edge.node.title}</h2>
<p>{edge.node.excerpt}</p>
<small>By {edge.node.author.name} on {edge.node.createdAt}</small>
</article>
)}
</For>
</div>
</div>
);
}

Infinite Scrolling

Implement infinite scrolling with intersection observer:

function InfiniteUserList(props: { $query: UserList_query$key }) {
const query = createPaginationFragment(
graphql`
fragment UserList_query on Query
@argumentDefinitions(
count: { type: "Int!", defaultValue: 10 },
cursor: { type: "String" },
)
@refetchable(queryName: "UserListPaginationQuery") {
users(
first: $count,
after: $cursor,
) @connection(key: "UserList_users") {
edges {
node {
id
name
email
avatar
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`,
() => props.$query,
);
let loadMoreRef: HTMLDivElement | undefined;
createEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && query.hasNext && !query.isLoadingNext) {
query.loadNext(10);
}
},
{ threshold: 0.1 }
);
if (loadMoreRef) {
observer.observe(loadMoreRef);
}
onCleanup(() => {
if (loadMoreRef) {
observer.unobserve(loadMoreRef);
}
});
});
return (
<div>
<div>
<For each={query()?.users.edges}>
{(edge) => (
<UserCard $user={edge.node} />
)}
</For>
</div>
<Show when={query.hasNext}>
<div ref={loadMoreRef}>
<Show
when={query.isLoadingNext}
fallback={<div>Scroll to load more...</div>}
>
<div>Loading more users...</div>
</Show>
</div>
</Show>
</div>
);
}

Search with Pagination

Combine search functionality with pagination:

function SearchResults(props: { $query: SearchResults_query$key }) {
const [searchTerm, setSearchTerm] = createSignal('');
const query = createPaginationFragment(
graphql`
fragment SearchResults_query on Query
@argumentDefinitions(
searchTerm: { type: "String!", defaultValue: "" },
count: { type: "Int!", defaultValue: 10 },
cursor: { type: "String" },
)
@refetchable(queryName: "SearchResultsPaginationQuery") {
searchUsers(
query: $searchTerm,
first: $count,
after: $cursor,
) @connection(key: "SearchResults_searchUsers") {
edges {
node {
id
name
email
avatar
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`,
() => props.$query,
);
const handleSearch = (term: string) => {
setSearchTerm(term);
query.refetch({ searchTerm: term });
};
return (
<div>
<input
type="text"
placeholder="Search users..."
value={searchTerm()}
onInput={(e) => handleSearch(e.target.value)}
/>
<div>
<For each={query()?.searchUsers.edges}>
{(edge) => (
<UserCard $user={edge.node} />
)}
</For>
</div>
<Show when={query.hasNext}>
<button
onClick={() => query.loadNext(10)}
disabled={query.isLoadingNext}
>
{query.isLoadingNext ? 'Loading...' : 'Load More Results'}
</button>
</Show>
</div>
);
}

Virtualization for Large Lists

For very large lists, consider using virtualization. It is recommended to use libraries like @tanstack/solid-virtual:

import { createVirtualizer } from '@tanstack/solid-virtual';
function VirtualizedUserList(props: { $query: UserList_query$key }) {
const query = createPaginationFragment(
graphql`
fragment UserList_query on Query
@refetchable(queryName: "UserListPaginationQuery") {
users(
first: $count,
after: $cursor,
) @connection(key: "UserList_users") {
edges {
node {
id
name
email
avatar
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`,
() => props.$query,
);
let parentRef: HTMLDivElement | undefined;
const virtualizer = createVirtualizer(() => ({
count: query()?.users.edges.length || 0,
getScrollElement: () => parentRef,
estimateSize: () => 80, // Estimated item height
overscan: 5,
}));
// Load more when approaching the end
createEffect(() => {
const items = virtualizer.getVirtualItems();
const lastItem = items[items.length - 1];
const edgesLength = query()?.users.edges.length || 0;
if (lastItem && lastItem.index >= edgesLength - 10) {
if (query.hasNext && !query.isLoadingNext) {
query.loadNext(20);
}
}
});
return (
<div
ref={parentRef}
style={{ height: '400px', overflow: 'auto' }}
>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
<For each={virtualizer.getVirtualItems()}>
{(virtualItem) => (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<Show when={query()?.users.edges[virtualItem.index]?.node}>
{(user) => <UserCard $user={user()} />}
</Show>
</div>
)}
</For>
</div>
</div>
);
}

Last updated: 8/13/25, 1:20 AM

Edit this page on GitHub
Solid RelaySolidJS Bindings for Relay
Community
github