Infinite Scroll in ICanUp initially looked correct. The first page of Posts loaded, I scrolled down, the request for page=2 completed, and the server returned the next records.
Then something unexpected happened: the Posts from page 1 disappeared and only the records from page 2 remained.
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.
What I Saw on the Page
Before scrolling, everything looked normal: the list contained records from the first page.
After the next page loaded, the server returned new Posts, but the previous list was no longer preserved.
The problem was not that page 2 failed to load. The problem was that page 2 became the new complete list.
Step | Actual | Expected |
|---|---|---|
Initial load | page=1 | page=1 |
First scroll | page=2 replaces page=1 | page=2 appends after page=1 |
Next scroll | only the latest page remains | all loaded pages remain |
Infinite Scroll Changes the Meaning of Pagination
For traditional pagination, replacing the list is completely normal. A user opens page 2 and sees the records from page 2.
Infinite Scroll has a different contract. The next page is a continuation of data that is already visible.
That difference determines whether the local list should be replaced or extended.
Traditional pagination: page=1 -> [1, 2, 3, 4]page=2 -> [5, 6, 7, 8] Infinite Scroll: page=1 -> [1, 2, 3, 4] page=2 ->[1, 2, 3, 4, 5, 6, 7, 8] page=3 ->[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]Replace and Append Are Different Operations
A simplified version of the bug can be represented as assigning every new response to the same list.
That is correct for the first page. For page 2 and later pages, it destroys the accumulated result.
posts.value = response.data;Infinite Scroll needs different semantics: the first page creates the initial state, while later pages extend it.
The page number affects not only the request URL but also how the local state should be updated.
posts.value = page === 1 ? response.data : [ ...posts.value, ...response.data, ];Page 1 Really Is Different
It may seem simpler to always append, but that creates another bug.
When the user opens another category, changes a filter, or switches locale, the existing results no longer belong to the new context.
An initial load is allowed to replace the list. A continuation of the same result set is not.
Pagination State Is Part of the List
An array of Posts is not enough. Infinite Scroll also needs to know which page has already been loaded and whether another page exists.
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.
const pagination = { currentPage: response.current_page, lastPage: response.last_page,}; const hasMore = pagination.currentPage < pagination.lastPage;The exact metadata shape depends on the application response, but the rule is the same.
The decision to load again should come from pagination state, not simply from another scroll event.
Append Introduces a Second Problem - Duplicates
Once the list starts accumulating pages, repeated requests need to be considered.
The same page can potentially be requested more than once because of UI state or closely timed load events.
Simply concatenating arrays can then render the same Post twice.
const merged = [ ...posts.value, ...response.data,]; posts.value = [ ...new Map( merged.map(post => [ post.id, post, ]), ).values(),];Map is only one possible implementation. The important invariant is that receiving the same Post again must not create another card.
Concurrent Loads Need a Guard Too
Infinite Scroll reacts automatically to page state, so another load should not start while the previous one is still running.
There is also no reason to send another request after the final page has been reached.
if ( loading.value || ! hasMore.value) { return;}A Context Change Must Reset the List
This was the other important behavior that needed to remain correct.
Category, filter, and locale define which Post result set the user is currently viewing.
If any of them changes, the accumulated list can no longer be continued.
A new context means a new page 1 and a new initial list.
const contextKey = [ locale, category, filter,].join(':'); if ( contextKey !== currentContextKey.value) { posts.value = []; currentPage.value = 1; currentContextKey.value = contextKey;}Event | List operation |
|---|---|
page=1 | Replace |
page=2, page=3... | Append |
Category changes | Reset and load page=1 |
Filter changes | Reset and load page=1 |
Locale changes | Reset and load page=1 |
Inertia.js Was Not the Real Problem
It is easy to initially blame the framework because the problem appears when new page data arrives.
In this case, the more important boundary was between server pagination and the local list state.
Inertia.js can deliver the new data, but the application still needs to know whether that data represents a new page state or a continuation of an accumulated result set.
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.
I Prefer an Explicit Update Mode
After this case, I prefer a model where the list update mode is visible from the context.
The first page or a new filter means reset. The next page of the same result set means append.
When that distinction is explicit, the behavior becomes easier to read, test, and change.
function applyPage( items, { reset = false, } = {},) { posts.value = reset ? items : mergeUnique( posts.value, items, );}What I Verify After the Fix
- The first page creates the initial Post list.
- After page 2 loads, Posts from page 1 remain visible.
- Page 3 and later pages append in the same way.
- Post ordering remains stable after several loads.
- Loading the same page again does not create duplicates.
- No additional request starts after the final page.
- Changing category resets the previous result set.
- Changing a filter resets the previous result set.
- Changing locale does not mix Posts from different locale contexts.
The Most Valuable Tests Cover a Sequence of States
A unit test for one array-merging function does not prove that Infinite Scroll works as a complete scenario.
The more useful checks cover a sequence: page 1, then page 2, then another page, plus separate context changes.
Scenario 1:page=1-> [1, 2, 3] page=2-> [1, 2, 3, 4, 5, 6] Scenario 2:page=1page=2change category-> old list cleared-> new page=1 Scenario 3:load page=2 twice-> no duplicated Post IDsWhat I Would Avoid
- Do not treat a successful page 2 response as proof that Infinite Scroll works.
- Do not use the same array replacement for the first and later pages.
- Do not append pages without duplicate protection.
- Do not continue an old list after category, filter, or locale changes.
- Do not determine whether another page exists only from a scroll event.
- Do not hide the symptom with extra requests before defining the state update rules.
Infinite Scroll is not simply pagination without a button. It changes the list update contract: the next page needs to continue the previous one.
What I Kept for Next Time
- Define different update modes for page 1 and later pages.
- Keep pagination metadata together with list state.
- Prevent concurrent next-page requests.
- Protect accumulated results from duplicate records.
- Treat category, filter, and locale as part of the result-set context.
- Start from a new page 1 when that context changes.
- Test sequences of several pages instead of only a single request.
Conclusion
In this case, the server returned the correct next page. The bug appeared when that data was applied to the existing list state.
Replacing page 1 with page 2 is normal for traditional pagination. For Infinite Scroll, it breaks the core behavior.
In ICanUp, the contract became simple: the first page creates the list, later pages extend it, repeated Posts do not create duplicates, and changing category, filter, or locale starts a new result set.
The distinction between reset and append turned out to matter more than the mechanism that actually loaded page 2.



