Skip to content

[2.x] fix: prevent discussion list cache wipe on back-nav to tag pages#4598

Merged
imorland merged 1 commit into2.xfrom
im/fix-4583-tag-pagination-reset
Apr 20, 2026
Merged

[2.x] fix: prevent discussion list cache wipe on back-nav to tag pages#4598
imorland merged 1 commit into2.xfrom
im/fix-4583-tag-pagination-reset

Conversation

@imorland
Copy link
Copy Markdown
Member

@imorland imorland commented Apr 20, 2026

Fixes #4583.

What

DiscussionListState.requestParams returned this.params.filter by reference. Any extender that mutated params.filter inside a requestParams extender callback (tags, subscriptions) silently wrote those mutations back into the stored state.

On the next mount of IndexPage, app.search.state.params() produced a fresh filter: {} (plus whatever tag slug etc.), and paramsChanged() compared that against the now-mutated stored filter via JSON.stringify — they differed, so refreshParams wiped the paginated cache and reloaded page 1. That's the regression reported in #4583: load-more a tag page, click a discussion, press back → you land on page 1.

/all was unaffected because nothing mutates params.filter there.

How

Clone this.params.filter into a fresh object when handing it to callers, so extender mutation lands on the per-request copy rather than the stored state. One-line change; diff's in DiscussionListState.ts.

Repro (before fix)

On a local 2.x instance, driven via Playwright against a tag with >40 discussions:

/t/general
  initial:        20 discussions
  after load-more: 60 discussions
  click discussion, press back
  after back:      0 discussions  🐞

After the fix, after back: 60 discussions on both /t/general and /all.

Root-cause trace (for reviewer context)

Inside paramsChanged during back-navigation, diagnostic logging showed:

changedKeys: ["filter: {\"tag\":\"general\"} -> {}"]

— the stored filter had been mutated to {tag: "general"} by the tags extender's requestParams extender on the first load, but the fresh params() call during the second mount returned {}. Mutation source: the extender writes params.filter.tag = this.params.tags, where params was the same reference as this.params.

Test

Added tests/unit/forum/states/DiscussionListState.test.ts that constructs a DiscussionListState, calls requestParams(), mutates the returned filter, and asserts this.params.filter is untouched. Verified the test fails on the pre-fix code and passes after.

Reviewers should focus on

  • Is { ...this.params.filter } the right level of cloning (shallow, filter only)? I considered deep-cloning the whole params but the only object-typed field in the requestParams output that extenders are known to mutate is filter. include is already a freshly-built array. Wider cloning can come later if another footgun surfaces.
  • Whether to apply the same defence to PaginatedListState.requestParams base class (which also returns this.params by reference). I've left that alone for now to keep this RC-safe; happy to do it as a follow-up if preferred.

Necessity

  • Has the problem that is being solved here been clearly explained?
  • If applicable, have various options for solving this problem been considered?
  • For core PRs, does this need to be in core, or could it be in an extension? (In core — fixes a contract between the base DLS and bundled extenders.)
  • Are we willing to maintain this for years / potentially forever?

Confirmed

  • Frontend changes: tested on a local Flarum installation (/t/<tag> and /all, both fixed; neither regressed).
  • Backend changes: tests are green (run composer test). — N/A, frontend-only.
  • Core developer confirmed locally this works as intended.
  • Tests have been added, or are not appropriate here.

Required changes

  • Related documentation PR: (none — internal implementation detail)

DiscussionListState.requestParams returned `this.params.filter` by
reference. Extenders (tags, subscriptions) then mutated that object to
build the outgoing API filter, silently writing their additions back
into the stored state. On the next mount, the freshly-supplied filter
no longer equalled the mutated stored copy, so `paramsChanged()` tripped
and the paginated cache was wiped — the visible "back button resets to
page 1" symptom on tag pages (#4583).

Clone `filter` before handing it out so extender mutation lands on the
per-request object, not on `this.params`.
@imorland imorland requested a review from a team as a code owner April 20, 2026 21:53
@imorland imorland modified the milestone: 2.0.0-rc.2 Apr 20, 2026
@imorland imorland merged commit 16446a3 into 2.x Apr 20, 2026
25 checks passed
@imorland imorland deleted the im/fix-4583-tag-pagination-reset branch April 20, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[2.x] On tag pages back button does not go back to page of the discussion

1 participant