Skip to content

ref(stores): Replace reflux with tanstack store - #128966

Closed
scttcper wants to merge 9 commits into
masterfrom
scttcper/tanstack-store-stores
Closed

scttcper wants to merge 9 commits into
masterfrom
scttcper/tanstack-store-stores

Conversation

@scttcper

@scttcper scttcper commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Swaps reflux for TanStack Store across every remaining store. Each store owns a TanStack Store (exposed as .store) and keeps its existing methods, so non-React callers (ConfigStore.get(...), openModal(), action creators) don't change. Components read with useSelector(XStore.store, selector) and only re-render on the fields they use. useLegacyStore is gone.

Resets used to be silent in reflux, now they notify like any other update. Specs that reset stores in afterEach while components were still mounted move that reset to beforeEach.

Pros

  • Already in the bundle through react-pacer (which shares 0.11.x with us) and react-form
  • Fits with the TanStack stuff we already use (query, form, pacer, devtools)
  • Maintained by the TanStack team rather than one person

Cons

  • Still pre-1.0. 0.11 renamed useStore to useSelector, and 1.0 is in alpha
  • setState only takes an updater, so every partial write is setState(prev => ({...prev, ...}))
  • useSelector needs a get() source, which clashes with our stores' own get methods, so components go through XStore.store
  • subscribe returns {unsubscribe} and listeners don't get the previous state

Draft for comparing against the zustand version in #128964. Server-data stores (projects, teams, groups) should still move to react-query over time.

Rebuilds every reflux store on a TanStack Store and drops reflux from
our dependencies. Each store keeps its exported name and methods, so
call sites and most tests don't change. `useLegacyStore` now wraps
`useSelector` from `@tanstack/react-store`, which we already ship
through react-form and react-pacer.

Reflux `reset()`/`init()` replaced state without notifying listeners,
and ~110 specs reset stores in `afterEach` while components are still
mounted. `createLegacyStore` keeps that behavior with a `resetState`
that skips subscribers, everything else goes through `setState`.

The few direct `.listen()` callers and the trigger spies in store specs
move to `subscribe`. TrialRequestedActions are plain functions now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Oct 27, 2026
@types/reflux, the jest unmock entry and a couple of comments still
pointed at reflux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📊 Type Coverage Diff

Metric Before After Delta
Coverage 95.24% 95.82% 🟢 +0.58%
Typed 140,986 141,842 🟢 +856
Untyped 7,042 6,193 🟢 -849
🔍 10 new type safety issues introduced

Non-null assertions (!) (4 new)

File Line Detail
static/app/stores/groupStore.tsx 211 GroupStore.items[idx]!
static/app/stores/tagStore.tsx 59 previousState[tagKey]!
static/gsApp/components/withSubscription.tsx 86 subscriptions[orgSlug]!
static/gsApp/stores/subscriptionStore.tsx 39 store.state[orgSlug]!

Type assertions (as) (6 new)

File Line Detail
static/app/stores/configStore.tsx 18 as Config — ({}) as Config
static/app/stores/groupStore.tsx 64 as Item[] — [] as Item[]
static/app/stores/groupStore.tsx 66 as Record<string, Record<string, boolean>> — {} as Record<string, Record<string, boolean>>
static/app/stores/organizationStore.tsx 29 as Organization — ( replace ? updatedOrg : {...store.state.organization, ...updatedOrg} ) as Orga…
static/gsApp/stores/subscriptionStore.tsx 27 as Record<string, boolean> — {} as Record<string, boolean>
static/gsApp/stores/subscriptionStore.tsx 28 as Record<string, GetCallback[]> — {} as Record<string, GetCallback[]>

This is informational only and does not block the PR.

scttcper and others added 4 commits October 27, 2026 15:52
Drops the createLegacyStore and useLegacyStore shims. Each store owns a
TanStack `Store` (exposed as `.store`) next to its existing methods, and
components read it with `useSelector(XStore.store, selector)` so they
only re-render on the fields they use. Subscribers use
`store.subscribe()` and its `unsubscribe`.

Resets notify subscribers now, which made specs that reset stores in
afterEach re-render (and refetch) mounted components. Those resets move
to beforeEach so each test starts from a clean store instead.

Also drops reflux from dependabot, knip and the codeowners baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Story previews

Preview the stories changed in this PR on the Vercel deployment:

Preview deployment: https://sentry-pab7y0pd5.sentry.dev

The root package never imports it, which knip flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The table renders before the events request resolves, so wait for the
pagination links instead of asserting on them right away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines +24 to +41
const store = new Store<State>(initialState);

function onUpdate(org: Organization, options?: {replace: true}): void;
function onUpdate(org: Partial<Organization>, options?: {replace?: false}): void;
function onUpdate(updatedOrg: Partial<Organization>, {replace = false} = {}) {
const organization = (
replace ? updatedOrg : {...store.state.organization, ...updatedOrg}
) as Organization;
store.setState(() => ({
loading: false,
dirty: false,
loading: true,
organization: null,
error: null,
errorType: null,
error: null,
organization,
}));

callSetOrganizationCallback(organization);
}

@scttcper scttcper Oct 27, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is kind of a nightmare.
oh i see why its doing this based on the previous types

@scttcper scttcper closed this Oct 29, 2026
@scttcper
scttcper deleted the scttcper/tanstack-store-stores branch October 29, 2026 18:06

This branch was successfully deployed

1 active deployment
Preview — ba8b4549 Deployed Oct 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant