<?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: Alyona Yakymiv</title>
    <description>The latest articles on DEV Community by Alyona Yakymiv (@xmarynkam).</description>
    <link>https://dev.to/xmarynkam</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4120694%2Fc5c66791-e05b-4c7d-9f87-0b0c0c0d3f20.jpg</url>
      <title>DEV Community: Alyona Yakymiv</title>
      <link>https://dev.to/xmarynkam</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xmarynkam"/>
    <language>en</language>
    <item>
      <title>Inertia.js Infinite Scroll: Why Page 2 Replaced Existing Posts Instead of Appending Them</title>
      <dc:creator>Alyona Yakymiv</dc:creator>
      <pubDate>Fri, 11 Sep 2026 10:16:07 +0000</pubDate>
      <link>https://dev.to/xmarynkam/inertiajs-infinite-scroll-why-page-2-replaced-existing-posts-instead-of-appending-them-1jad</link>
      <guid>https://dev.to/xmarynkam/inertiajs-infinite-scroll-why-page-2-replaced-existing-posts-instead-of-appending-them-1jad</guid>
      <description>&lt;p&gt;Infinite Scroll in ICanUp initially looked correct. The first page of Posts loaded, I scrolled down, the request for &lt;code&gt;page=2&lt;/code&gt; completed, and the server returned the next records.&lt;/p&gt;

&lt;p&gt;Then something unexpected happened: the Posts from page 1 disappeared, and only the records from page 2 remained.&lt;/p&gt;

&lt;p&gt;The interesting part was that pagination itself worked. The data arrived. The problem was how the next page became part of the list that was already on screen.&lt;/p&gt;

&lt;p&gt;For Infinite Scroll, successfully loading page 2 is not enough. The application needs an explicit rule for when a response &lt;strong&gt;replaces&lt;/strong&gt; the list and when it &lt;strong&gt;extends&lt;/strong&gt; it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Saw on the Page
&lt;/h2&gt;

&lt;p&gt;Before scrolling, everything looked normal: the list contained records from the first page.&lt;/p&gt;

&lt;p&gt;After the next page loaded, the server returned new Posts, but the previous list was no longer preserved.&lt;/p&gt;

&lt;p&gt;The problem was not that page 2 failed to load.&lt;/p&gt;

&lt;p&gt;The problem was that &lt;strong&gt;page 2 became the new complete list&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Actual&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial load&lt;/td&gt;
&lt;td&gt;&lt;code&gt;page=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;page=1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First scroll&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;page=2&lt;/code&gt; replaces &lt;code&gt;page=1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;page=2&lt;/code&gt; appends after &lt;code&gt;page=1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next scroll&lt;/td&gt;
&lt;td&gt;Only the latest page remains&lt;/td&gt;
&lt;td&gt;All loaded pages remain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Infinite Scroll Changes the Meaning of Pagination
&lt;/h2&gt;

&lt;p&gt;For traditional pagination, replacing the list is completely normal. A user opens page 2 and sees the records from page 2.&lt;/p&gt;

&lt;p&gt;Infinite Scroll has a different contract.&lt;/p&gt;

&lt;p&gt;The next page is a &lt;strong&gt;continuation of data that is already visible&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That difference determines whether the local list should be replaced or extended.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two Different Pagination Contracts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Traditional pagination:

page=1 -&amp;gt; [1, 2, 3, 4]
page=2 -&amp;gt; [5, 6, 7, 8]


Infinite Scroll:

page=1 -&amp;gt; [1, 2, 3, 4]

page=2 -&amp;gt;
[1, 2, 3, 4, 5, 6, 7, 8]

page=3 -&amp;gt;
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Replace and Append Are Different Operations
&lt;/h2&gt;

&lt;p&gt;A simplified version of the bug can be represented as assigning every new response to the same list.&lt;/p&gt;

&lt;p&gt;That is correct for the first page.&lt;/p&gt;

&lt;p&gt;For page 2 and later pages, it destroys the accumulated result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplified List Replacement
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Infinite Scroll needs different semantics: the first page creates the initial state, while later pages extend it.&lt;/p&gt;

&lt;p&gt;The page number affects not only the request URL but also how the local state should be updated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replace for Page 1, Append for Later Pages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These snippets demonstrate the state contract rather than reproducing a specific production class.&lt;/p&gt;

&lt;p&gt;Page 1 creates the list, while &lt;code&gt;page &amp;gt; 1&lt;/code&gt; extends it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Page 1 Really Is Different
&lt;/h2&gt;

&lt;p&gt;It may seem simpler to always append, but that creates another bug.&lt;/p&gt;

&lt;p&gt;When the user opens another category, changes a filter, or switches locale, the existing results no longer belong to the new context.&lt;/p&gt;

&lt;p&gt;An initial load is allowed to replace the list.&lt;/p&gt;

&lt;p&gt;A continuation of the same result set is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pagination State Is Part of the List
&lt;/h2&gt;

&lt;p&gt;An array of Posts is not enough.&lt;/p&gt;

&lt;p&gt;Infinite Scroll also needs to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which page has already been loaded;&lt;/li&gt;
&lt;li&gt;whether another page exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pagination metadata is as important as the records themselves. Without it, the frontend can request the same page repeatedly or continue after the final page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplified Pagination State
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pagination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastPage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact metadata shape depends on the application response, but the rule is the same.&lt;/p&gt;

&lt;p&gt;The decision to load again should come from pagination state, not simply from another scroll event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Append Introduces a Second Problem: Duplicates
&lt;/h2&gt;

&lt;p&gt;Once the list starts accumulating pages, repeated requests need to be considered.&lt;/p&gt;

&lt;p&gt;The same page can potentially be requested more than once because of UI state or closely timed load events.&lt;/p&gt;

&lt;p&gt;Simply concatenating arrays can then render the same Post twice.&lt;/p&gt;

&lt;h3&gt;
  
  
  One Way to Prevent Duplicate Post IDs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Map&lt;/code&gt; is only one possible implementation.&lt;/p&gt;

&lt;p&gt;The important invariant is that receiving the same Post again must not create another card.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrent Loads Need a Guard Too
&lt;/h2&gt;

&lt;p&gt;Infinite Scroll reacts automatically to page state, so another load should not start while the previous one is still running.&lt;/p&gt;

&lt;p&gt;There is also no reason to send another request after the final page has been reached.&lt;/p&gt;

&lt;h3&gt;
  
  
  Guard Against an Unnecessary Next Request
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nx"&gt;hasMore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents overlapping requests and avoids requesting pages after the result set has already ended.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Context Change Must Reset the List
&lt;/h2&gt;

&lt;p&gt;This was the other important behavior that needed to remain correct.&lt;/p&gt;

&lt;p&gt;Category, filter, and locale define which Post result set the user is currently viewing.&lt;/p&gt;

&lt;p&gt;If any of them changes, the accumulated list can no longer be continued.&lt;/p&gt;

&lt;p&gt;A new context means:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a new page 1 and a new initial list.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplified Context Reset
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contextKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;contextKey&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;currentContextKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nx"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;currentContextKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contextKey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting behavior becomes explicit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;List operation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;page=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;page=2&lt;/code&gt;, &lt;code&gt;page=3&lt;/code&gt;, ...&lt;/td&gt;
&lt;td&gt;Append&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Category changes&lt;/td&gt;
&lt;td&gt;Reset and load &lt;code&gt;page=1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter changes&lt;/td&gt;
&lt;td&gt;Reset and load &lt;code&gt;page=1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locale changes&lt;/td&gt;
&lt;td&gt;Reset and load &lt;code&gt;page=1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Inertia.js Was Not the Real Problem
&lt;/h2&gt;

&lt;p&gt;It is easy to initially blame the framework because the problem appears when new page data arrives.&lt;/p&gt;

&lt;p&gt;In this case, the more important boundary was between &lt;strong&gt;server pagination&lt;/strong&gt; and &lt;strong&gt;local list state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Inertia.js can deliver the new data, but the application still needs to know whether that data represents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a new page state; or&lt;/li&gt;
&lt;li&gt;a continuation of an accumulated result set.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was not the first Inertia.js problem that made me look more carefully at the server-client boundary. I previously wrote about a case where meta tags existed in the browser but were missing from the initial HTML.&lt;/p&gt;

&lt;p&gt;The framework can transport the data correctly while the application still applies the wrong state transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Prefer an Explicit Update Mode
&lt;/h2&gt;

&lt;p&gt;After this case, I prefer a model where the list update mode is visible from the context.&lt;/p&gt;

&lt;p&gt;The first page or a new filter means &lt;strong&gt;reset&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The next page of the same result set means &lt;strong&gt;append&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When that distinction is explicit, the behavior becomes easier to read, test, and change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Explicit List Update Mode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;reset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;reset&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;mergeUnique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation may differ, but the intent is now visible at the call site.&lt;/p&gt;

&lt;p&gt;That makes the state transition much harder to misunderstand.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Verify After the Fix
&lt;/h2&gt;

&lt;p&gt;After changing the list update behavior, I verify the entire sequence rather than only checking whether the next request succeeds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first page creates the initial Post list.&lt;/li&gt;
&lt;li&gt;After page 2 loads, Posts from page 1 remain visible.&lt;/li&gt;
&lt;li&gt;Page 3 and later pages append in the same way.&lt;/li&gt;
&lt;li&gt;Post ordering remains stable after several loads.&lt;/li&gt;
&lt;li&gt;Loading the same page again does not create duplicates.&lt;/li&gt;
&lt;li&gt;No additional request starts after the final page.&lt;/li&gt;
&lt;li&gt;Changing category resets the previous result set.&lt;/li&gt;
&lt;li&gt;Changing a filter resets the previous result set.&lt;/li&gt;
&lt;li&gt;Changing locale does not mix Posts from different locale contexts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Most Valuable Tests Cover a Sequence of States
&lt;/h2&gt;

&lt;p&gt;A unit test for one array-merging function does not prove that Infinite Scroll works as a complete scenario.&lt;/p&gt;

&lt;p&gt;The more useful checks cover a sequence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;page 1;&lt;/li&gt;
&lt;li&gt;then page 2;&lt;/li&gt;
&lt;li&gt;then another page;&lt;/li&gt;
&lt;li&gt;plus separate context changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Scenarios That Should Remain Stable
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scenario 1:

page=1
-&amp;gt; [1, 2, 3]

page=2
-&amp;gt; [1, 2, 3, 4, 5, 6]


Scenario 2:

page=1
page=2
change category

-&amp;gt; old list cleared
-&amp;gt; new page=1


Scenario 3:

load page=2 twice

-&amp;gt; no duplicated Post IDs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the fix, success means more than completing the next request.&lt;/p&gt;

&lt;p&gt;The complete list, its ordering, and its current context all need to remain correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Avoid
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Do not treat a successful page 2 response as proof that Infinite Scroll works.&lt;/li&gt;
&lt;li&gt;Do not use the same array replacement for the first and later pages.&lt;/li&gt;
&lt;li&gt;Do not append pages without duplicate protection.&lt;/li&gt;
&lt;li&gt;Do not continue an old list after category, filter, or locale changes.&lt;/li&gt;
&lt;li&gt;Do not determine whether another page exists only from a scroll event.&lt;/li&gt;
&lt;li&gt;Do not hide the symptom with extra requests before defining the state update rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Infinite Scroll is not simply pagination without a button.&lt;/p&gt;

&lt;p&gt;It changes the list update contract: &lt;strong&gt;the next page needs to continue the previous one&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Kept for Next Time
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Define different update modes for page 1 and later pages.&lt;/li&gt;
&lt;li&gt;Keep pagination metadata together with list state.&lt;/li&gt;
&lt;li&gt;Prevent concurrent next-page requests.&lt;/li&gt;
&lt;li&gt;Protect accumulated results from duplicate records.&lt;/li&gt;
&lt;li&gt;Treat category, filter, and locale as part of the result-set context.&lt;/li&gt;
&lt;li&gt;Start from a new page 1 when that context changes.&lt;/li&gt;
&lt;li&gt;Test sequences of several pages instead of only a single request.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this case, the server returned the correct next page.&lt;/p&gt;

&lt;p&gt;The bug appeared when that data was applied to the existing list state.&lt;/p&gt;

&lt;p&gt;Replacing page 1 with page 2 is normal for traditional pagination. For Infinite Scroll, it breaks the core behavior.&lt;/p&gt;

&lt;p&gt;In ICanUp, the contract became simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the first page creates the list;&lt;/li&gt;
&lt;li&gt;later pages extend it;&lt;/li&gt;
&lt;li&gt;repeated Posts do not create duplicates;&lt;/li&gt;
&lt;li&gt;changing category, filter, or locale starts a new result set.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The distinction between &lt;strong&gt;reset&lt;/strong&gt; and &lt;strong&gt;append&lt;/strong&gt; turned out to matter more than the mechanism that actually loaded page 2.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>javascript</category>
      <category>vue</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
