<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ikjot Singh Dhody</title>
    <description>The latest articles on DEV Community by Ikjot Singh Dhody (@ikjot2605).</description>
    <link>https://dev.to/ikjot2605</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1010782%2Ff668620b-b5bb-466f-8154-3d3c46efdaa6.JPG</url>
      <title>DEV Community: Ikjot Singh Dhody</title>
      <link>https://dev.to/ikjot2605</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ikjot2605"/>
    <language>en</language>
    <item>
      <title>The Internals of Bidirectional Pagination in Relay: A Deep Dive</title>
      <dc:creator>Ikjot Singh Dhody</dc:creator>
      <pubDate>Sun, 27 Jul 2025 22:30:56 +0000</pubDate>
      <link>https://dev.to/ikjot2605/the-internals-of-bidirectional-pagination-in-relay-a-deep-dive-9f4</link>
      <guid>https://dev.to/ikjot2605/the-internals-of-bidirectional-pagination-in-relay-a-deep-dive-9f4</guid>
      <description>&lt;p&gt;Relay is a GraphQL client built by Meta, designed for large-scale, high-performance apps. Unlike more flexible alternatives like Apollo, Relay enforces stricter rules for how you write queries and manage your local store — which means fewer hidden bugs, but a steeper learning curve.&lt;/p&gt;

&lt;p&gt;One of Relay’s biggest selling points is its battle-tested, “black-box” implementation of &lt;strong&gt;bidirectional pagination&lt;/strong&gt;. If you’ve ever used it, you’ve probably wondered: &lt;em&gt;What actually happens behind the scenes?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;How does Relay merge pages when you scroll &lt;strong&gt;up and down&lt;/strong&gt; at the same time?&lt;br&gt;&lt;br&gt;
Bidirectional pagination is powerful — but it can feel like magic. Let’s break it down.&lt;/p&gt;


&lt;h2&gt;
  
  
  A Quick Primer on Bidirectional Pagination
&lt;/h2&gt;

&lt;p&gt;Before we get into the &lt;em&gt;bi&lt;/em&gt; in bidirectional, let’s start with the basics. When you’re dealing with huge lists — like your Twitter/X feed or a WhatsApp chat — it’s not practical to load everything at once. &lt;strong&gt;Pagination&lt;/strong&gt; lets your app fetch just a chunk of data first, then grab more as you scroll.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bidirectional pagination&lt;/strong&gt; is simply a smarter version: you can scroll both ways — fetching older or newer data around a point of interest.&lt;/p&gt;

&lt;p&gt;Take WhatsApp: imagine you search for an old message from two months ago. You tap that message — now you might want to scroll &lt;em&gt;up&lt;/em&gt; to see what came before, or &lt;em&gt;down&lt;/em&gt; to see what came after. Bidirectional pagination makes this smooth and efficient.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ip4nooxw6qc2n5ssy9t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ip4nooxw6qc2n5ssy9t.png" alt="Bidirectional pagination example showing a WhatsApp-like interface with messages" width="800" height="780"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Think of it like two doors: you can open more of the list from the top or the bottom, but it all connects in the same hallway.&lt;/p&gt;

&lt;p&gt;Merging these pages correctly — without duplicates, gaps, or weird jumps — is tricky. But Relay handles this for you with its &lt;strong&gt;connections system&lt;/strong&gt;. Let’s see how.&lt;/p&gt;


&lt;h2&gt;
  
  
  Connections, Edges, and Cursors: The Building Blocks
&lt;/h2&gt;

&lt;p&gt;Relay wants you to think of your data as a &lt;strong&gt;graph&lt;/strong&gt;. A list of related items — like a user’s friends — is just another branch in that graph.&lt;/p&gt;

&lt;p&gt;Let’s start simple. Suppose you want to fetch a user and their friends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;friends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This asks for the first 10 friends of user &lt;code&gt;123&lt;/code&gt;. Straightforward. But how does your app know there are &lt;em&gt;more&lt;/em&gt;? How do you fetch them — and where do you &lt;em&gt;merge&lt;/em&gt; them?&lt;/p&gt;

&lt;p&gt;That’s where &lt;strong&gt;connections&lt;/strong&gt; come in. Relay (and GraphQL best practice) breaks lists into a clear structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Edges&lt;/strong&gt;: Each edge holds a single item (&lt;code&gt;node&lt;/code&gt;) plus a &lt;strong&gt;cursor&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pageInfo&lt;/strong&gt;: Special metadata that says if there’s more data &lt;em&gt;before&lt;/em&gt; or &lt;em&gt;after&lt;/em&gt; this chunk, and gives you the cursors to get it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So your real query looks more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;friends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UserFriends_friends"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;pageInfo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;hasNextPage&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;hasPreviousPage&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;startCursor&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;endCursor&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  How does this actually work?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;@connection&lt;/code&gt; directive tells Relay: &lt;strong&gt;“Track this list. Merge new pages cleanly when I fetch more.”&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edges&lt;/strong&gt; hold each friend’s info plus a &lt;strong&gt;cursor&lt;/strong&gt; — like a bookmark for where you are in the list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pageInfo&lt;/strong&gt; says what’s next or previous — and provides the cursors to continue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you want to load more friends, you don’t just call &lt;code&gt;first: 10&lt;/code&gt; again.&lt;br&gt;&lt;br&gt;
You use a cursor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;To scroll forward:&lt;br&gt;&lt;br&gt;
  friends(first: 10, after: "")&lt;br&gt;&lt;br&gt;
→ Relay merges these new edges at the &lt;strong&gt;end&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To scroll backward:&lt;br&gt;&lt;br&gt;
  friends(last: 10, before: "")&lt;br&gt;&lt;br&gt;
→ Relay merges these new edges at the &lt;strong&gt;start&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is &lt;strong&gt;bidirectional pagination&lt;/strong&gt; in action — Relay’s internals ensure the new edges fit perfectly with the old ones, without overlaps or missing data.&lt;/p&gt;




&lt;h3&gt;
  
  
  But do you write &lt;code&gt;before&lt;/code&gt; and &lt;code&gt;after&lt;/code&gt; by hand?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Nope!&lt;/strong&gt; That’s where Relay’s smart client helpers come in.&lt;/p&gt;

&lt;p&gt;In practice, you don’t write this whole query directly in your component.&lt;br&gt;&lt;br&gt;
Relay encourages you to break it into &lt;strong&gt;fragments&lt;/strong&gt; — each fragment describes exactly what a single component needs.&lt;/p&gt;

&lt;p&gt;To paginate, you wrap the list in a &lt;strong&gt;pagination fragment&lt;/strong&gt; and use Relay’s &lt;code&gt;usePaginationFragment&lt;/code&gt; hook.&lt;/p&gt;

&lt;p&gt;When your component runs, Relay gives you helpers like &lt;code&gt;loadNext&lt;/code&gt; and &lt;code&gt;loadPrevious&lt;/code&gt;. Under the hood, these automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pick up the right cursor (&lt;code&gt;endCursor&lt;/code&gt; or &lt;code&gt;startCursor&lt;/code&gt;) from &lt;code&gt;pageInfo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Plug it into &lt;code&gt;after&lt;/code&gt; or &lt;code&gt;before&lt;/code&gt; for you
&lt;/li&gt;
&lt;li&gt;Fire the next request for more edges
&lt;/li&gt;
&lt;li&gt;Merge the new edges into the right place&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No manual cursors. No manual merging. You just call &lt;code&gt;loadNext&lt;/code&gt; or &lt;code&gt;loadPrevious&lt;/code&gt; — Relay does the rest.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Up next:&lt;/strong&gt; Let’s peek under the hood at &lt;em&gt;how&lt;/em&gt; Relay’s store and connection handlers keep your list smooth and consistent, even when you scroll in two directions at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Behind the Scenes: How Relay Actually Merges Pages
&lt;/h3&gt;

&lt;p&gt;So far, we’ve seen how your GraphQL query is structured — and how &lt;code&gt;usePaginationFragment&lt;/code&gt; handles fetching new pages in both directions.&lt;/p&gt;

&lt;p&gt;But &lt;em&gt;how&lt;/em&gt; does Relay actually keep this list in sync inside its local store?&lt;br&gt;&lt;br&gt;
How does it know where to insert new edges? And how does it make sure your UI updates correctly, with no duplicates or flickers?&lt;/p&gt;

&lt;p&gt;Let’s peek into the internals.&lt;/p&gt;




&lt;h2&gt;
  
  
  Under the Hood: Relay’s Store Architecture
&lt;/h2&gt;

&lt;p&gt;Before we zoom in on how connections merge pages, it helps to understand the key pieces of Relay’s architecture.&lt;br&gt;&lt;br&gt;
Relay isn’t just a GraphQL client — it’s also a local data store, a change tracker, and a consistency manager &lt;em&gt;all in one&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;At the heart of it are a few core pieces:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc8u8b36lxzi30ytx5036.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc8u8b36lxzi30ytx5036.png" alt="Relay Store Architecture" width="721" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Relay Store
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Relay Store&lt;/strong&gt; is the single source of truth for your app’s active GraphQL data.&lt;br&gt;&lt;br&gt;
It holds &lt;em&gt;normalized records&lt;/em&gt; for all queries, fragments, and subscriptions that your components are currently using.&lt;/p&gt;

&lt;p&gt;When no part of your app references certain records anymore, Relay’s garbage collector can safely clean them up — keeping your Store lean and memory-efficient (This can be customized based on your requirements).&lt;/p&gt;

&lt;p&gt;Every query, mutation, or subscription you run updates the Store, and Relay makes sure your components always see the freshest version of the data they depend on.&lt;/p&gt;




&lt;h3&gt;
  
  
  RecordSource
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;RecordSource&lt;/strong&gt; is the low-level map of all your data.&lt;br&gt;&lt;br&gt;
It’s basically a giant key-value store that keeps all your Relay data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keys:&lt;/strong&gt; Unique IDs for your records (like &lt;code&gt;User:123&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Values:&lt;/strong&gt; The actual fields and nested references for that record&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it like a normalized cache: no duplicates, no wasted data =&amp;gt; Memory Efficient!&lt;/p&gt;




&lt;h3&gt;
  
  
  Record Proxies
&lt;/h3&gt;

&lt;p&gt;When you write a mutation or pagination updater, you don’t directly mutate the Store.&lt;br&gt;&lt;br&gt;
Instead, Relay gives you &lt;strong&gt;proxies&lt;/strong&gt; — safe wrappers like &lt;code&gt;RecordProxy&lt;/code&gt; and &lt;code&gt;RelayRecordSourceProxy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These proxies let you &lt;em&gt;describe&lt;/em&gt; changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add or remove edges&lt;/li&gt;
&lt;li&gt;Update a field on a node&lt;/li&gt;
&lt;li&gt;Insert new nodes into a connection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Relay then commits those changes in a controlled way via the PublishQueue.&lt;br&gt;
This guarantees consistency and avoids race conditions.&lt;/p&gt;




&lt;h3&gt;
  
  
  Publish Queue
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;PublishQueue&lt;/strong&gt; is Relay’s message queue.&lt;br&gt;&lt;br&gt;
When a network response comes back — or you run an optimistic update — Relay doesn’t update changes directly into the Store.&lt;br&gt;&lt;br&gt;
Instead, the changes go through the PublishQueue:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It figures out what changed
&lt;/li&gt;
&lt;li&gt;It updates the RecordSource&lt;/li&gt;
&lt;li&gt;It notifies any UI components watching those records&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is how Relay guarantees that your UI stays in sync &lt;em&gt;without you wiring manual cache updates&lt;/em&gt;.&lt;/p&gt;




&lt;p&gt;To summarize, Relay keeps all your data in a normalized graph called the RecordSource, managed by the Store.&lt;br&gt;
When you run a pagination update, Relay uses a PublishQueue and safe Proxy objects to merge the new page into the right place — without breaking anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Together&lt;/strong&gt;, these parts mean that when you load the &lt;em&gt;next page&lt;/em&gt; or the &lt;em&gt;previous page&lt;/em&gt;, the new edges flow through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Network → PublishQueue → RecordSource → Store → Components&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your list updates automatically, your cache stays normalized, and duplicate edges get handled automatically.&lt;/p&gt;

&lt;p&gt;Now that you know how Relay’s store works, let’s see how the &lt;strong&gt;Connection Handler&lt;/strong&gt; fits in — and how it merges new pages into your connection in both directions, without conflicts or gaps.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Connection Handler
&lt;/h2&gt;

&lt;p&gt;At the heart of this is &lt;strong&gt;Relay’s Connection Handler&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When Relay sees the &lt;code&gt;@connection&lt;/code&gt; directive, it knows this field is special. It stores that list in a normalized way inside its local store. The Connection Handler keeps track of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The current edges&lt;/li&gt;
&lt;li&gt;The pageInfo (startCursor, endCursor, hasNextPage, hasPreviousPage)&lt;/li&gt;
&lt;li&gt;The connection key you provided (like &lt;code&gt;"UserFriends_friends"&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whenever you call &lt;code&gt;loadNext&lt;/code&gt; or &lt;code&gt;loadPrevious&lt;/code&gt;, Relay:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads the right cursor (&lt;code&gt;endCursor&lt;/code&gt; or &lt;code&gt;startCursor&lt;/code&gt;) from &lt;code&gt;pageInfo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Builds a new query with the &lt;code&gt;after&lt;/code&gt; or &lt;code&gt;before&lt;/code&gt; argument
&lt;/li&gt;
&lt;li&gt;Sends the request and gets new edges + new &lt;code&gt;pageInfo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merges&lt;/strong&gt; the new edges into the connection:

&lt;ul&gt;
&lt;li&gt;If you paged &lt;strong&gt;forward&lt;/strong&gt;, it appends to the end.&lt;/li&gt;
&lt;li&gt;If you paged &lt;strong&gt;backward&lt;/strong&gt;, it prepends to the start.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This merge happens in Relay’s &lt;strong&gt;normalized store&lt;/strong&gt;, which means other components reusing the same connection see the updated list automatically — no manual cache updates needed.&lt;/p&gt;




&lt;h3&gt;
  
  
  How Does It Avoid Duplicates?
&lt;/h3&gt;

&lt;p&gt;Relay’s store is &lt;em&gt;keyed by IDs&lt;/em&gt;. Each &lt;code&gt;node&lt;/code&gt; under &lt;code&gt;edges&lt;/code&gt; has a unique ID (&lt;code&gt;id&lt;/code&gt;). If the server accidentally sends overlapping items (like your page 1 ends at user 10 and your page 2 starts at user 10 again), Relay’s merge logic de-duplicates it using these IDs.&lt;/p&gt;

&lt;p&gt;This is why using stable IDs on your server is so important — it ensures Relay can merge pages without showing the same item twice.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why This Matters
&lt;/h3&gt;

&lt;p&gt;You don’t have to wire any of this yourself.&lt;br&gt;&lt;br&gt;
You don’t have to manually splice arrays or juggle “current page” states.&lt;/p&gt;

&lt;p&gt;Relay’s Connection Handler + &lt;code&gt;usePaginationFragment&lt;/code&gt; means your list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loads in both directions
&lt;/li&gt;
&lt;li&gt;De-duplicates edges
&lt;/li&gt;
&lt;li&gt;Merges pages smoothly
&lt;/li&gt;
&lt;li&gt;Reacts instantly in your UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the magic that makes bidirectional pagination “just work.”&lt;/p&gt;




&lt;h2&gt;
  
  
  Inside ConnectionHandler.update(): How Relay Actually Merges Pages
&lt;/h2&gt;

&lt;p&gt;Once you've fetched paginated data using &lt;code&gt;@connection&lt;/code&gt;, Relay delegates rendering to a handler. This handler uses an &lt;code&gt;update()&lt;/code&gt; function that determines how to merge new edges into the client-side store while maintaining pagination metadata like cursors and &lt;code&gt;hasNextPage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let’s break it down:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyap1lkancmc4dcsldxgm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyap1lkancmc4dcsldxgm.png" alt="Relay Store Architecture" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Identify the Source
&lt;/h3&gt;

&lt;p&gt;Relay provides &lt;code&gt;payload&lt;/code&gt;, which tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which record you're updating (&lt;code&gt;dataID&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The connection field on the server (&lt;code&gt;fieldKey&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The client-side handle (&lt;code&gt;handleKey&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The updater uses this info to grab the record from the store and fetch the server-side connection field.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2: Determine Client-Side Connection
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;If there's &lt;strong&gt;no existing client connection&lt;/strong&gt;, we create one using &lt;code&gt;generateClientID()&lt;/code&gt;, copy fields from the server, and assign a fresh &lt;code&gt;pageInfo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If it &lt;strong&gt;already exists&lt;/strong&gt;, we reuse it, optionally re-linking it if the &lt;code&gt;handleKey&lt;/code&gt; isn’t set yet.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Step 3: Merge Edges Intelligently
&lt;/h3&gt;

&lt;p&gt;Based on &lt;code&gt;after&lt;/code&gt; or &lt;code&gt;before&lt;/code&gt; args, Relay decides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are we paginating &lt;strong&gt;forward or backward&lt;/strong&gt;?&lt;/li&gt;
&lt;li&gt;Is it a &lt;strong&gt;refetch or incremental fetch&lt;/strong&gt;?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Relay deduplicates nodes (by &lt;code&gt;node.id&lt;/code&gt;) and merges new and previous edges accordingly.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 4: Sync PageInfo
&lt;/h3&gt;

&lt;p&gt;Relay then updates &lt;code&gt;pageInfo&lt;/code&gt; depending on the type of pagination:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Replace it entirely&lt;/strong&gt; (in case of full refetch)&lt;/li&gt;
&lt;li&gt;Or selectively update:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hasNextPage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;endCursor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hasPreviousPage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;startCursor&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;This is crucial for enabling infinite scrolls or paged lists to keep fetching smoothly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;During &lt;strong&gt;forward pagination&lt;/strong&gt;, only &lt;code&gt;hasNextPage&lt;/code&gt; and &lt;code&gt;endCursor&lt;/code&gt; are updated.
&lt;/li&gt;
&lt;li&gt;During &lt;strong&gt;backward pagination&lt;/strong&gt;, only &lt;code&gt;hasPreviousPage&lt;/code&gt; and &lt;code&gt;startCursor&lt;/code&gt; are updated.
&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;initial page loads&lt;/strong&gt; (when neither &lt;code&gt;after&lt;/code&gt; nor &lt;code&gt;before&lt;/code&gt; is provided in the query), the entire &lt;code&gt;pageInfo&lt;/code&gt; from the server is directly copied into the client-side connection.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Why This Matters
&lt;/h3&gt;

&lt;p&gt;This updater is the &lt;strong&gt;secret sauce&lt;/strong&gt; behind how Relay seamlessly handles pagination without you having to micromanage lists, cursors, or duplicates.&lt;/p&gt;

&lt;p&gt;By walking through this lifecycle and mapping server responses to normalized client records, Relay guarantees your UI stays in sync with your backend — no matter how complex your pagination gets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next:&lt;/strong&gt; Let’s wrap this up with some &lt;em&gt;gotchas&lt;/em&gt;, best practices, and a simple starter boilerplate you can adapt for your own app!&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls and Best Practices
&lt;/h2&gt;

&lt;p&gt;Relay’s pagination feels magical once it clicks — but there are a few pitfalls to watch for, especially when building advanced pagination handlers or wiring it all up from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Pitfalls
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You must use the &lt;code&gt;@connection&lt;/code&gt; handler&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Relay only applies pagination logic and handlers when &lt;code&gt;@connection(key: "...")&lt;/code&gt; is present on your fragment field. Without it, edges and pageInfo won’t be stored or updated correctly. Relay won't even handle/call the updater as we saw above if we don't add this directive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Missing &lt;code&gt;pageInfo&lt;/code&gt; in schema = silent bugs&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Ensure your GraphQL schema includes &lt;code&gt;startCursor&lt;/code&gt;, &lt;code&gt;endCursor&lt;/code&gt;, &lt;code&gt;hasNextPage&lt;/code&gt;, and &lt;code&gt;hasPreviousPage&lt;/code&gt; in the &lt;code&gt;pageInfo&lt;/code&gt; field. Relay expects these to exist to properly manage pagination. If you don't add these fields - in case of first page fetches/your connection may not be updated to the store as you'd expect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;node.id&lt;/code&gt; is used for deduping&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If your edges don’t contain unique &lt;code&gt;node.id&lt;/code&gt;s, Relay’s merge logic may misbehave or allow duplicates. Always ensure &lt;code&gt;node.id&lt;/code&gt; is globally unique.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Refetch ≠ Pagination&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Calling &lt;code&gt;refetch&lt;/code&gt; replaces the whole connection, while &lt;code&gt;loadNext&lt;/code&gt;/&lt;code&gt;loadPrevious&lt;/code&gt; appends/prepends edges. Use the right one depending on your UX needs. For pagination, you should use &lt;code&gt;loadNext&lt;/code&gt;/&lt;code&gt;loadPrevious&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cursors are auto-injected by Relay&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
You don’t manually pass &lt;code&gt;after&lt;/code&gt; or &lt;code&gt;before&lt;/code&gt; to the server. When using &lt;code&gt;usePaginationFragment&lt;/code&gt;, Relay pulls the right cursor from the &lt;code&gt;pageInfo&lt;/code&gt; and injects it into the next query.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Best Practices
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use stable connection keys&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Avoid interpolating dynamic values into &lt;code&gt;@connection(key: "...")&lt;/code&gt;. Prefer static keys and use &lt;code&gt;filters&lt;/code&gt; to differentiate if needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Always normalize edges and node IDs&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Even in backend responses, ensure your &lt;code&gt;edges[].node.id&lt;/code&gt; is present and unique. This plays well with Relay’s store and deduplication.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debug with &lt;code&gt;__id&lt;/code&gt; and DevTools&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
You can inspect how connections and edges are structured in the Relay store using the &lt;code&gt;__id&lt;/code&gt; field or Relay DevTools. Very useful when things don’t behave as expected. You could also add breakpoints in the relevant Relay files in your DevTool sources (&lt;code&gt;connectionHandler.js&lt;/code&gt;, &lt;code&gt;RelayModernStore.js&lt;/code&gt;, etc.) to look at your records - and see what is the current status after a query/mutation. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Relay’s pagination system is deceptively powerful — once you understand how the connection model, cursors, and store updates interact, it feels almost invisible.&lt;/p&gt;

&lt;p&gt;By using &lt;code&gt;@connection&lt;/code&gt; and relying on the ConnectionHandler’s update cycle, you gain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic edge merging and deduplication&lt;/li&gt;
&lt;li&gt;Cursor-aware infinite scrolls&lt;/li&gt;
&lt;li&gt;Minimal manual state management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Understanding this lifecycle not only helps in debugging but also empowers you to confidently build scalable, performant lists in any app.&lt;/p&gt;

&lt;p&gt;Embrace the Relay — let it handle the complexity, so you can focus on product.&lt;/p&gt;

&lt;h2&gt;
  
  
  References and Further Reading
&lt;/h2&gt;

&lt;p&gt;Here are some official docs, articles, and community resources that provide deeper insights into Relay's pagination system and store architecture:&lt;/p&gt;

&lt;h3&gt;
  
  
  Official Documentation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://relay.dev/docs/guided-tour/list-data/pagination/" rel="noopener noreferrer"&gt;Relay Pagination (Relay Docs)&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Covers &lt;code&gt;usePaginationFragment&lt;/code&gt;, connections, and loading more data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://relay.dev/docs/next/guided-tour/list-data/advanced-pagination/" rel="noopener noreferrer"&gt;Relay Advanced Pagination Usage&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Relay API docs for bidirectional pagination/ paginating over multiple connections.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://relay.dev/docs/api-reference/store/" rel="noopener noreferrer"&gt;Relay Store APIs&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Explore APIs available to update/read store in Relay&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://relay.dev/docs/tutorial/connections-pagination/" rel="noopener noreferrer"&gt;@connection Directive&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Quick tutorial on how connections/pagination/cursors etc. work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://relay.dev/graphql/connections.htm" rel="noopener noreferrer"&gt;GraphQL Cursor Connections Specification&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The foundational spec that Relay's pagination is based on. Very useful to understand how Meta expects you to use Relay for pagination.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Debugging &amp;amp; Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://chrome.google.com/webstore/detail/relay-devtools/" rel="noopener noreferrer"&gt;Relay DevTools Chrome Extension&lt;/a&gt;&lt;/strong&gt;
Inspect your Relay store, queries, and connections in real time.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>graphql</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>react</category>
    </item>
  </channel>
</rss>
