typescript-utility-types
Use when typeScript utility types, mapped types, and advanced type manipulation. Use when creating flexible, type-safe TypeScript code.
relay-pagination
Use when relay pagination with cursor-based pagination, infinite scroll, load more patterns, and connection protocols.
Full skill instructions
Master Relay's cursor-based pagination for efficiently loading and displaying large datasets with infinite scroll and load more patterns.
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.
# 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!
}
// 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;
// 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>
);
}
// 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>
);
}
// 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>
);
}
// 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.length} of {data.posts.totalCount} posts
</div>
{data.posts.edges.map(({ node }) => (
<PostCard key={node.id} post={node} />
))}
{hasNext && (
<button onClick={() => loadNext(10)} disabled={isLoadingNext}>
{isLoadingNext ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
}
// OptimisticPaginationPosts.jsx
const CreatePostMutation = graphql`
mutation OptimisticPaginationCreatePostMutation(
$input: CreatePostInput!
$connections: [ID!]!
) {
createPost(input: $input) {
postEdge @prependEdge(connections: $connections) {
cursor
node {
id
...PostCard_post
}
}
}
}
`;
function OptimisticPaginationPosts({ query }) {
const { data } = usePaginationFragment(PostsFragment, query);
const [commit] = useMutation(CreatePostMutation);
const connectionID = ConnectionHandler.getConnectionID(
'client:root',
'Posts_posts'
);
const handleCreate = (title, body) => {
commit({
variables: {
input: { title, body },
connections: [connectionID]
},
optimisticResponse: {
createPost: {
postEdge: {
cursor: 'temp-cursor',
node: {
id: `temp-${Date.now()}`,
title,
body,
createdAt: new Date().toISOString(),
author: {
id: currentUser.id,
name: currentUser.name
}
}
}
}
}
});
};
return (
<div>
<CreatePostForm onSubmit={handleCreate} />
{data.posts.edges.map(({ node }) => (
<PostCard key={node.id} post={node} />
))}
</div>
);
}
// TabbedPosts.jsx
const TabbedPostsFragment = graphql`
fragment TabbedPosts_user on User
@refetchable(queryName: "TabbedPostsQuery")
@argumentDefinitions(
draftsFirst: { type: "Int", defaultValue: 10 }
draftsAfter: { type: "String" }
publishedFirst: { type: "Int", defaultValue: 10 }
publishedAfter: { type: "String" }
) {
draftPosts: posts(
first: $draftsFirst
after: $draftsAfter
status: DRAFT
)
@connection(key: "TabbedPosts_draftPosts") {
edges {
node {
id
...PostCard_post
}
}
}
publishedPosts: posts(
first: $publishedFirst
after: $publishedAfter
status: PUBLISHED
)
@connection(key: "TabbedPosts_publishedPosts") {
edges {
node {
id
...PostCard_post
}
}
}
}
`;
function TabbedPosts({ user }) {
const [activeTab, setActiveTab] = useState('published');
const { data } = usePaginationFragment(TabbedPostsFragment, user);
const posts =
activeTab === 'draft' ? data.draftPosts : data.publishedPosts;
return (
<div>
<Tabs value={activeTab} onChange={setActiveTab}>
<Tab value="published">Published</Tab>
<Tab value="draft">Drafts</Tab>
</Tabs>
{posts.edges.map(({ node }) => (
<PostCard key={node.id} post={node} />
))}
</div>
);
}
// VirtualizedPosts.jsx
import { useVirtualizer } from '@tanstack/react-virtual';
import { graphql, usePaginationFragment } from 'react-relay';
const VirtualizedPostsFragment = graphql`
fragment VirtualizedPosts_query on Query
@refetchable(queryName: "VirtualizedPostsQuery")
@argumentDefinitions(
first: { type: "Int", defaultValue: 50 }
after: { type: "String" }
) {
posts(first: $first, after: $after)
@connection(key: "VirtualizedPosts_posts") {
edges {
node {
id
...PostCard_post
}
}
}
}
`;
function VirtualizedPosts({ query }) {
const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment(
VirtualizedPostsFragment,
query
);
const parentRef = useRef();
const posts = data.posts.edges.map(e => e.node);
const virtualizer = useVirtualizer({
count: posts.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 200,
overscan: 5
});
useEffect(() => {
const [lastItem] = [...virtualizer.getVirtualItems()].reverse();
if (!lastItem) return;
if (
lastItem.index >= posts.length - 1 &&
hasNext &&
!isLoadingNext
) {
loadNext(50);
}
}, [
hasNext,
loadNext,
isLoadingNext,
posts.length,
virtualizer.getVirtualItems()
]);
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative'
}}
>
{virtualizer.getVirtualItems().map(virtualItem => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`
}}
>
<PostCard post={posts[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
// PaginationStateManager.jsx
function PaginationStateManager({ query }) {
const {
data,
loadNext,
hasNext,
isLoadingNext,
refetch
} = usePaginationFragment(PostsFragment, query);
const [paginationState, setPaginationState] = useState({
currentPage: 1,
itemsPerPage: 10,
totalLoaded: 0
});
const handleLoadMore = () => {
const itemsToLoad = paginationState.itemsPerPage;
loadNext(itemsToLoad);
setPaginationState(prev => ({
...prev,
currentPage: prev.currentPage + 1,
totalLoaded: prev.totalLoaded + itemsToLoad
}));
};
const handleChangePageSize = (newSize) => {
setPaginationState(prev => ({
...prev,
itemsPerPage: newSize
}));
refetch({
first: newSize,
after: null
});
};
return (
<div>
<div>
Page {paginationState.currentPage} -
Loaded {paginationState.totalLoaded} items
</div>
<select
value={paginationState.itemsPerPage}
onChange={(e) => handleChangePageSize(Number(e.target.value))}
>
<option value={10}>10 per page</option>
<option value={25}>25 per page</option>
<option value={50}>50 per page</option>
</select>
{data.posts.edges.map(({ node }) => (
<PostCard key={node.id} post={node} />
))}
{hasNext && (
<button onClick={handleLoadMore} disabled={isLoadingNext}>
Load More
</button>
)}
</div>
);
}
// hooks/usePagination.js
import { useState, useCallback } from 'react';
import { usePaginationFragment } from 'react-relay';
export function usePagination(fragment, fragmentRef, options = {}) {
const {
onLoadMore,
onLoadPrevious,
onRefetch,
pageSize = 10
} = options;
const {
data,
loadNext,
loadPrevious,
hasNext,
hasPrevious,
isLoadingNext,
isLoadingPrevious,
refetch
} = usePaginationFragment(fragment, fragmentRef);
const [page, setPage] = useState(1);
const handleLoadNext = useCallback(() => {
loadNext(pageSize);
setPage(p => p + 1);
onLoadMore?.();
}, [loadNext, pageSize, onLoadMore]);
const handleLoadPrevious = useCallback(() => {
loadPrevious(pageSize);
setPage(p => Math.max(1, p - 1));
onLoadPrevious?.();
}, [loadPrevious, pageSize, onLoadPrevious]);
const handleRefetch = useCallback((variables) => {
refetch(variables);
setPage(1);
onRefetch?.();
}, [refetch, onRefetch]);
return {
data,
page,
hasNext,
hasPrevious,
isLoadingNext,
isLoadingPrevious,
loadNext: handleLoadNext,
loadPrevious: handleLoadPrevious,
refetch: handleRefetch
};
}
// Usage
function PostsList({ query }) {
const {
data,
page,
hasNext,
loadNext,
refetch
} = usePagination(PostsFragment, query, {
pageSize: 20,
onLoadMore: () => console.log('Loaded more'),
onRefetch: () => console.log('Refetched')
});
return (
<div>
<div>Page {page}</div>
<button onClick={() => refetch({ first: 20 })}>Refresh</button>
{data.posts.edges.map(({ node }) => (
<PostCard key={node.id} post={node} />
))}
{hasNext && <button onClick={loadNext}>Load More</button>}
</div>
);
}
Use when typeScript utility types, mapped types, and advanced type manipulation. Use when creating flexible, type-safe TypeScript code.
Use when writing shell scripts that need to run across different systems, shells, or environments. Covers POSIX compatibility and platform differences.
Use when optimizing Tailwind CSS for production, reducing bundle size, and improving performance. Covers PurgeCSS, JIT mode, and build optimization.
Generate production-ready code from a Figma component or frame using the Figma MCP server
Use when integrating Dialyzer into development workflows and CI/CD pipelines for Erlang/Elixir projects.
Use when ruboCop configuration including YAML config, departments, inheritance, and customization.
Use when Elixir OTP patterns including GenServer, Supervisor, Agent, and Task. Use when building concurrent, fault-tolerant Elixir applications.
Use when creating custom Ameba rules for Crystal code analysis including rule development, AST traversal, issue reporting, and rule testing.
Use when swift's optional handling patterns including optional binding, chaining, nil coalescing, and modern approaches to safely working with optional values while avoiding common pitfalls and force unwrapping.
Use when annotating code with structured metadata, tags, and markers for AI-assisted development workflows. Covers annotation formats, semantic tags, and integration with development tools.
Use when scala collections including immutable/mutable variants, List, Vector, Set, Map operations, collection transformations, lazy evaluation with views, parallel collections, and custom collection builders for efficient data processing.
Use when creating custom React Flow nodes, edges, and handles. Covers custom node components, resizable nodes, toolbars, and advanced customization.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience