From 83c552db77f5edf07978e7fd80b88d4003cb8d88 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Tue, 27 Oct 2026 15:31:25 -0700 Subject: [PATCH 1/7] ref(stores): Replace reflux with tanstack store 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 --- package.json | 2 +- pnpm-lock.yaml | 56 ++-- static/AGENTS.md | 4 +- .../app/components/pageFilters/store.spec.tsx | 12 +- static/app/components/pageFilters/store.tsx | 185 +++++------ static/app/package.json | 2 +- static/app/stores/IssueListCacheStore.tsx | 44 +-- static/app/stores/configStore.tsx | 50 +-- static/app/stores/createLegacyStore.tsx | 54 +++ static/app/stores/demoWalkthroughStore.tsx | 42 +-- static/app/stores/groupStore.spec.tsx | 30 +- static/app/stores/groupStore.tsx | 309 +++++++----------- static/app/stores/guideStore.tsx | 146 ++++----- static/app/stores/modalStore.tsx | 54 ++- static/app/stores/onboardingDrawerStore.tsx | 48 +-- static/app/stores/organizationStore.tsx | 98 +++--- static/app/stores/organizationsStore.tsx | 76 ++--- static/app/stores/projectsStore.tsx | 158 +++------ static/app/stores/tagStore.spec.tsx | 6 +- static/app/stores/tagStore.tsx | 39 +-- static/app/stores/teamStore.tsx | 159 ++++----- static/app/stores/types.tsx | 21 -- static/app/stores/useLegacyStore.tsx | 29 +- static/app/types/reflux.d.ts | 11 - .../setOrganizationCallback.tsx | 2 +- .../app/views/issueDetails/groupDetails.tsx | 4 +- .../gsApp/actions/trialRequestedActions.tsx | 10 +- .../components/trialStartedSidebarItem.tsx | 5 +- static/gsApp/components/withSubscription.tsx | 13 +- static/gsApp/package.json | 2 +- static/gsApp/stores/subscriptionStore.tsx | 91 ++---- static/gsApp/stores/trialRequestedStore.tsx | 31 +- 32 files changed, 707 insertions(+), 1086 deletions(-) create mode 100644 static/app/stores/createLegacyStore.tsx delete mode 100644 static/app/stores/types.tsx delete mode 100644 static/app/types/reflux.d.ts diff --git a/package.json b/package.json index 1cbd71c1e1cf..fddec9b8ee2e 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@sentry/sqlish": "1.0.1", "@sentry/webpack-plugin": "5.4.1", "@swc/plugin-emotion": "19.0.0", + "@tanstack/react-store": "0.11.2", "@types/gtag.js": "^0.0.12", "@types/jest": "30.0.0", "@types/react": "19.3.0", @@ -86,7 +87,6 @@ "less-loader": "^13.0.0", "peggy": "5.1.1", "react": "19.3.0", - "reflux": "0.4.1", "rehype-expressive-code": "^0.44.2", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 071b041c86cd..c12a26f51c5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -241,6 +241,9 @@ importers: '@swc/plugin-emotion': specifier: 19.0.0 version: 19.0.0 + '@tanstack/react-store': + specifier: 0.11.2 + version: 0.11.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@types/gtag.js': specifier: ^0.0.12 version: 0.0.12 @@ -283,9 +286,6 @@ importers: react: specifier: 19.3.0 version: 19.3.0 - reflux: - specifier: 0.4.1 - version: 0.4.1(react@19.3.0) rehype-expressive-code: specifier: ^0.44.2 version: 0.44.2 @@ -640,6 +640,9 @@ importers: '@tanstack/react-query-persist-client': specifier: 5.101.4 version: 5.101.4(@tanstack/react-query@5.101.4(react@19.3.0))(react@19.3.0) + '@tanstack/react-store': + specifier: 0.11.2 + version: 0.11.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@tanstack/react-virtual': specifier: 3.14.6 version: 3.14.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -784,9 +787,6 @@ importers: react-select: specifier: 4.3.1 version: 4.3.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@8.1.1) - reflux: - specifier: 0.4.1 - version: 0.4.1(react@19.3.0) screenfull: specifier: ^6.0.2 version: 6.0.2 @@ -996,6 +996,9 @@ importers: '@tanstack/react-query': specifier: 5.101.4 version: 5.101.4(react@19.3.0) + '@tanstack/react-store': + specifier: 0.11.2 + version: 0.11.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0) color: specifier: 5.0.2 version: 5.0.2 @@ -1032,9 +1035,6 @@ importers: react-router: specifier: 8.4.0 version: 8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - reflux: - specifier: 0.4.1 - version: 0.4.1(react@19.3.0) sentry: specifier: workspace:@sentry-internal/app@* version: link:../app @@ -4531,8 +4531,8 @@ packages: peerDependencies: react: ^18 || ^19 - '@tanstack/react-store@0.11.0': - resolution: {integrity: sha512-tX4YXh3PDkmpvGQWkWqKpzs/MSqbtuwY9dWdWhtV9Q50PmO+jOkUKIWIX4G85dwt7lxdHLXsiaEKPdKmC8F41w==} + '@tanstack/react-store@0.11.2': + resolution: {integrity: sha512-oj5O5DmiMWE+ih+SioX2ayC1kbK7d3fpLx0B75fPJr9nY3zU+hoW010yr64qXsY/9fyiLVE2bn5mGNV3owm8RQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4557,6 +4557,9 @@ packages: '@tanstack/store@0.11.0': resolution: {integrity: sha512-WlzzCt3xi0G6pCAJu1U+2jiECwabETDpQDi3hfkFZvJii9AuZqEKbOiVarX1/bWhTNjU486yQtJCCasi/0q+Cw==} + '@tanstack/store@0.11.2': + resolution: {integrity: sha512-sJ4mjol8uQsHV0gOJzzjwXfh2Fwm+Sz0+8deqiTm4jGbMdjzNSW+xZCFm0kUa870uhd8yi+DpKnZb5Kc4apa0Q==} + '@tanstack/store@0.9.3': resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} @@ -6171,9 +6174,6 @@ packages: event-emitter@0.3.5: resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} - eventemitter3@1.2.0: - resolution: {integrity: sha512-DOFqA1MF46fmZl2xtzXR3MPCRsXqgoFqdXcrCVYM3JNnfUeHTm/fh/v/iU7gBFpwkuBmoJPAm5GuhdDfSEJMJA==} - exec-sh@0.3.6: resolution: {integrity: sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==} @@ -8249,14 +8249,6 @@ packages: resolution: {integrity: sha512-r0Ay04Snci87djAsI4U+WNRcSw5S4pOH7qFjd/veA5gC7TbqESR3tcj28ia95L/fYUDw11JKP7uqUKUAfVvV5Q==} engines: {node: '>= 0.4'} - reflux-core@0.3.0: - resolution: {integrity: sha512-mOOxvUQcNE3HvYXHt2SoqlVso5OtQL82zl60PfR1An57UU4ho30h0hMO6fh5hsDcfxywo74+TdForH+pfCZGeQ==} - - reflux@0.4.1: - resolution: {integrity: sha512-xrAUNOKzNegiYHpj9Vv5eCdV3HZ1H0f9T5TXcTVk1dbOSMBL4ppUM+3Gvw1HCiLRfe+IAtj7k4DXe/r7XcswlQ==} - peerDependencies: - react: '>=0.13.0 <16' - regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -12111,7 +12103,7 @@ snapshots: '@tanstack/react-pacer@0.22.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: '@tanstack/pacer': 0.21.1 - '@tanstack/react-store': 0.11.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/react-store': 0.11.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0) react: 19.3.0 react-dom: 19.3.0(react@19.3.0) @@ -12132,9 +12124,9 @@ snapshots: '@tanstack/query-core': 5.101.4 react: 19.3.0 - '@tanstack/react-store@0.11.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + '@tanstack/react-store@0.11.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: - '@tanstack/store': 0.11.0 + '@tanstack/store': 0.11.2 react: 19.3.0 react-dom: 19.3.0(react@19.3.0) use-sync-external-store: 1.6.0(react@19.3.0) @@ -12159,6 +12151,8 @@ snapshots: '@tanstack/store@0.11.0': {} + '@tanstack/store@0.11.2': {} + '@tanstack/store@0.9.3': {} '@tanstack/virtual-core@3.17.4': {} @@ -13867,8 +13861,6 @@ snapshots: d: 1.0.2 es5-ext: 0.10.64 - eventemitter3@1.2.0: {} - exec-sh@0.3.6: {} execa@4.1.0: @@ -16678,16 +16670,6 @@ snapshots: gopd: 1.2.0 which-builtin-type: 1.2.1 - reflux-core@0.3.0: - dependencies: - eventemitter3: 1.2.0 - - reflux@0.4.1(react@19.3.0): - dependencies: - eventemitter3: 1.2.0 - react: 19.3.0 - reflux-core: 0.3.0 - regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 diff --git a/static/AGENTS.md b/static/AGENTS.md index 3f4a2040ffaa..a539da2ccd45 100644 --- a/static/AGENTS.md +++ b/static/AGENTS.md @@ -8,7 +8,7 @@ - **Framework**: React 19 - **Build Tool**: Rsbuild (powered by Rspack) - **Package management**: pnpm -- **State Management**: Reflux, React Query (TanStack Query) +- **State Management**: Zustand, React Query (TanStack Query) - **Styling**: Emotion (CSS-in-JS), Less - **Testing**: Jest, React Testing Library @@ -45,7 +45,7 @@ Use `apiOptions` with `useQuery` from TanStack Query; **never** use the deprecat ## General Frontend Rules -1. NO new Reflux stores +1. NO new stores in `static/app/stores` (use React Query for server data) 2. NO class components 3. NO CSS files (use [core components](./app/components/core/) or Emotion in edge cases) 4. ALWAYS use TypeScript diff --git a/static/app/components/pageFilters/store.spec.tsx b/static/app/components/pageFilters/store.spec.tsx index 7b462090a89b..7bc55ee851f1 100644 --- a/static/app/components/pageFilters/store.spec.tsx +++ b/static/app/components/pageFilters/store.spec.tsx @@ -52,13 +52,15 @@ describe('PageFiltersStore', () => { }); it('does not update if projects has same value', async () => { - const triggerSpy = jest.spyOn(PageFiltersStore, 'trigger'); + const triggerSpy = jest.fn(); + const unsubscribe = PageFiltersStore.subscribe(triggerSpy); PageFiltersStore.updateProjects([1], []); await waitFor(() => PageFiltersStore.getState().selection.projects[0] === 1); PageFiltersStore.updateProjects([1], []); await tick(); expect(triggerSpy).toHaveBeenCalledTimes(1); + unsubscribe(); }); it('updateDateTime()', async () => { @@ -111,7 +113,8 @@ describe('PageFiltersStore', () => { const start = new Date(now); const end = new Date(now + 1000); - const triggerSpy = jest.spyOn(PageFiltersStore, 'trigger'); + const triggerSpy = jest.fn(); + const unsubscribe = PageFiltersStore.subscribe(triggerSpy); PageFiltersStore.updateDateTime({end, start, period: null, utc: null}); await waitFor(() => PageFiltersStore.getState().selection.datetime.start === start); @@ -125,6 +128,7 @@ describe('PageFiltersStore', () => { await tick(); expect(triggerSpy).toHaveBeenCalledTimes(1); + unsubscribe(); }); it('updateEnvironments()', async () => { @@ -135,8 +139,9 @@ describe('PageFiltersStore', () => { }); it('does not update if environments has same value', async () => { + const triggerSpy = jest.fn(); + const unsubscribe = PageFiltersStore.subscribe(triggerSpy); PageFiltersStore.updateEnvironments(['alpha']); - const triggerSpy = jest.spyOn(PageFiltersStore, 'trigger'); await waitFor( () => PageFiltersStore.getState().selection.environments[0] === 'alpha' ); @@ -144,6 +149,7 @@ describe('PageFiltersStore', () => { PageFiltersStore.updateEnvironments(['alpha']); await tick(); expect(triggerSpy).toHaveBeenCalledTimes(1); + unsubscribe(); }); it('updatePersistence()', async () => { diff --git a/static/app/components/pageFilters/store.tsx b/static/app/components/pageFilters/store.tsx index afc6996705a0..0256c559f603 100644 --- a/static/app/components/pageFilters/store.tsx +++ b/static/app/components/pageFilters/store.tsx @@ -1,8 +1,6 @@ -import {createStore} from 'reflux'; - import type {PageFilterAdjustments} from 'sentry/components/pageFilters/adjustments'; import {getDefaultPageFilterSelection} from 'sentry/components/pageFilters/constants'; -import type {StrictStoreDefinition} from 'sentry/stores/types'; +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {PageFilters, PinnedPageFilter, PageFilterDatetime} from 'sentry/types/core'; import {valueIsEqual} from 'sentry/utils/object/valueIsEqual'; @@ -82,162 +80,137 @@ interface PageFiltersState { shouldPersist: boolean; } -interface PageFiltersStoreDefinition extends StrictStoreDefinition { - /** - * Call this *after* the update that caused the adjustment, since updating a - * filter clears its adjustments. - */ - addAdjustment( - filter: F, - adjustment: NonNullable - ): void; - onInitializeUrlState( - newSelection: PageFilters, - persist?: boolean, - adjustments?: PageFilterAdjustments - ): void; - onReset(): void; - pin(filter: PinnedPageFilter, pin: boolean): void; - reset(selection?: PageFilters): void; - updateDateTime(datetime: PageFilterDatetime): void; - updateEnvironments(environments: string[] | null): void; - updatePersistence(shouldPersist: boolean): void; - updateProjects(projects: PageFilters['projects'], environments: null | string[]): void; -} - -const storeConfig: PageFiltersStoreDefinition = { - state: { +const store = createLegacyStore({ + isReady: false, + selection: getDefaultPageFilterSelection(), + pinnedFilters: new Set(), + shouldPersist: true, + adjustments: {}, +}); + +function getResetState(selection?: PageFilters): PageFiltersState { + return { + ...store.getState(), isReady: false, - selection: getDefaultPageFilterSelection(), + selection: selection || getDefaultPageFilterSelection(), pinnedFilters: new Set(), - shouldPersist: true, adjustments: {}, - }, + }; +} - init() { - // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux - // listeners due to their leaky nature in tests. +export const PageFiltersStore = { + ...store, - this.reset(this.state.selection); + init() { + PageFiltersStore.reset(store.getState().selection); }, - reset(selection) { - this.state = { - ...this.state, - isReady: false, - selection: selection || getDefaultPageFilterSelection(), - pinnedFilters: new Set(), - adjustments: {}, - }; + reset(selection?: PageFilters) { + store.resetState(getResetState(selection)); }, /** * Initializes the page filters store data */ - onInitializeUrlState(newSelection, persist = true, adjustments = {}) { - this.state = { - ...this.state, + onInitializeUrlState( + newSelection: PageFilters, + persist = true, + adjustments: PageFilterAdjustments = {} + ) { + store.setState(prev => ({ + ...prev, isReady: true, selection: newSelection, pinnedFilters: new Set(['projects', 'environments', 'datetime']), shouldPersist: persist, adjustments, - }; - this.trigger(this.getState()); - }, - - getState() { - return this.state; + })); }, onReset() { - this.reset(); - this.trigger(this.getState()); + store.setState(() => getResetState()); }, updatePersistence(shouldPersist: boolean) { - this.state = {...this.state, shouldPersist}; - this.trigger(this.getState()); + store.setState(prev => ({...prev, shouldPersist})); }, - addAdjustment(filter, adjustment) { - if (valueIsEqual(this.state.adjustments[filter], adjustment, true)) { + /** + * Call this *after* the update that caused the adjustment, since updating a + * filter clears its adjustments. + */ + addAdjustment( + filter: F, + adjustment: NonNullable + ) { + const {adjustments} = store.getState(); + if (valueIsEqual(adjustments[filter], adjustment, true)) { return; } - this.state = { - ...this.state, - adjustments: {...this.state.adjustments, [filter]: adjustment}, - }; - this.trigger(this.getState()); + store.setState(prev => ({ + ...prev, + adjustments: {...adjustments, [filter]: adjustment}, + })); }, - updateProjects(projects = [], environments = null) { - if (valueIsEqual(this.state.selection.projects, projects)) { + updateProjects( + projects: PageFilters['projects'] = [], + environments: null | string[] = null + ) { + const state = store.getState(); + if (valueIsEqual(state.selection.projects, projects)) { return; } - const selection = { - ...this.state.selection, - projects, - environments: - environments === null ? this.state.selection.environments : environments, - }; - this.state = { - ...this.state, - selection, + store.setState(prev => ({ + ...prev, + selection: { + ...state.selection, + projects, + environments: environments === null ? state.selection.environments : environments, + }, adjustments: clearAdjustments( - this.state.adjustments, + state.adjustments, environments === null ? ['projects'] : ['projects', 'environments'] ), - }; - this.trigger(this.getState()); + })); }, - updateDateTime(newDateTime) { - if (datetimeHasSameValue(this.state.selection.datetime, newDateTime)) { + updateDateTime(newDateTime: PageFilterDatetime) { + const state = store.getState(); + if (datetimeHasSameValue(state.selection.datetime, newDateTime)) { return; } - this.state = { - ...this.state, - selection: { - ...this.state.selection, - datetime: newDateTime, - }, - adjustments: clearAdjustments(this.state.adjustments, ['datetime']), - }; - this.trigger(this.getState()); + store.setState(prev => ({ + ...prev, + selection: {...state.selection, datetime: newDateTime}, + adjustments: clearAdjustments(state.adjustments, ['datetime']), + })); }, - updateEnvironments(environments) { - if (valueIsEqual(this.state.selection.environments, environments)) { + updateEnvironments(environments: string[] | null) { + const state = store.getState(); + if (valueIsEqual(state.selection.environments, environments)) { return; } - this.state = { - ...this.state, - selection: { - ...this.state.selection, - environments: environments ?? [], - }, - adjustments: clearAdjustments(this.state.adjustments, ['environments']), - }; - - this.trigger(this.getState()); + store.setState(prev => ({ + ...prev, + selection: {...state.selection, environments: environments ?? []}, + adjustments: clearAdjustments(state.adjustments, ['environments']), + })); }, - pin(filter, pin) { - const newPinnedFilters = new Set(this.state.pinnedFilters); + pin(filter: PinnedPageFilter, pin: boolean) { + const newPinnedFilters = new Set(store.getState().pinnedFilters); if (pin) { newPinnedFilters.add(filter); } else { newPinnedFilters.delete(filter); } - this.state = {...this.state, pinnedFilters: newPinnedFilters}; - this.trigger(this.getState()); + store.setState(prev => ({...prev, pinnedFilters: newPinnedFilters})); }, }; - -export const PageFiltersStore = createStore(storeConfig); diff --git a/static/app/package.json b/static/app/package.json index c87961d75047..032d929afba5 100644 --- a/static/app/package.json +++ b/static/app/package.json @@ -68,6 +68,7 @@ "@tanstack/react-query": "5.101.4", "@tanstack/react-query-devtools": "5.101.4", "@tanstack/react-query-persist-client": "5.101.4", + "@tanstack/react-store": "0.11.2", "@tanstack/react-virtual": "3.14.6", "ansi-sequence-parser": "^1.1.3", "base64-arraybuffer": "^1.0.1", @@ -116,7 +117,6 @@ "react-lazyload": "^3.2.1", "react-router": "8.4.0", "react-select": "4.3.1", - "reflux": "0.4.1", "screenfull": "^6.0.2", "sentry": "workspace:@sentry-internal/app@*", "sonner": "2.0.8", diff --git a/static/app/stores/IssueListCacheStore.tsx b/static/app/stores/IssueListCacheStore.tsx index 533e13497061..8545fb3c2dc4 100644 --- a/static/app/stores/IssueListCacheStore.tsx +++ b/static/app/stores/IssueListCacheStore.tsx @@ -1,10 +1,8 @@ import isEqual from 'lodash/isEqual'; -import {createStore} from 'reflux'; +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {Group} from 'sentry/types/group'; -import type {StrictStoreDefinition} from './types'; - /** * The type here doesn't really matter it just needs to be compared via isEqual */ @@ -36,52 +34,34 @@ interface IssueListCacheState { // 30 seconds const CACHE_EXPIRATION = 30 * 1000; -interface IssueListCacheStoreDefinition extends StrictStoreDefinition { - getFromCache(params: LooseParamsType): IssueListCache | null; - markGroupAsSeen(groupId: string): void; - reset(): void; - save(params: LooseParamsType, data: IssueListCache): void; -} - -const storeConfig: IssueListCacheStoreDefinition = { - state: null, +const store = createLegacyStore(null); - init() {}, +export const IssueListCacheStore = { + ...store, reset() { - this.state = null; + store.resetState(null); }, save(params: LooseParamsType, data: IssueListCache) { - this.state = { - params, - cache: data, - expiration: Date.now() + CACHE_EXPIRATION, - }; + store.resetState({params, cache: data, expiration: Date.now() + CACHE_EXPIRATION}); }, getFromCache(params: LooseParamsType) { - if ( - this.state && - this.state.expiration > Date.now() && - isEqual(this.state?.params, params) - ) { - return this.state.cache; + const state = store.getState(); + if (state && state.expiration > Date.now() && isEqual(state.params, params)) { + return state.cache; } return null; }, - getState() { - return this.state; - }, - markGroupAsSeen(groupId: string) { - const matchingGroup = this.state?.cache.groups.find(group => group.id === groupId); + const matchingGroup = store + .getState() + ?.cache.groups.find(group => group.id === groupId); if (matchingGroup) { matchingGroup.hasSeen = true; } }, }; - -export const IssueListCacheStore = createStore(storeConfig); diff --git a/static/app/stores/configStore.tsx b/static/app/stores/configStore.tsx index 77cfb51631a6..fd07aee07d7a 100644 --- a/static/app/stores/configStore.tsx +++ b/static/app/stores/configStore.tsx @@ -1,58 +1,42 @@ import moment from 'moment-timezone'; -import {createStore} from 'reflux'; +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {Config} from 'sentry/types/system'; -import type {StrictStoreDefinition} from './types'; +// When the app is booted we will _immediately_ hydrate the config store, +// effecively ensuring this is not empty. +const store = createLegacyStore({} as Config); -interface ConfigStoreDefinition extends StrictStoreDefinition { - get(key: K): Config[K]; - loadInitialData(config: Config): void; - set(key: K, value: Config[K]): void; -} - -const storeConfig: ConfigStoreDefinition = { - // When the app is booted we will _immediately_ hydrate the config store, - // effecively ensuring this is not empty. - state: {} as Config, +export const ConfigStore = { + ...store, init(): void { - // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux - // listeners due to their leaky nature in tests. - - this.state = {} as Config; + store.resetState({} as Config); }, - get(key) { - return this.state[key]; + get(key: K): Config[K] { + return store.getState()[key]; }, - set(key, value) { - this.state = {...this.state, [key]: value}; - this.trigger({[key]: value}); + set(key: K, value: Config[K]): void { + store.setState(() => ({...store.getState(), [key]: value})); }, - loadInitialData(config): void { + loadInitialData(config: Config): void { const shouldUseDarkMode = config.user?.options.theme === 'dark'; - this.state = { + const state: Config = { ...config, features: new Set(config.features || []), theme: shouldUseDarkMode ? 'dark' : 'light', }; // TODO(dcramer): abstract this out of ConfigStore - if (this.state.user) { - this.state.user.permissions = new Set(this.state.user.permissions); - moment.tz.setDefault(this.state.user.options.timezone); + if (state.user) { + state.user.permissions = new Set(state.user.permissions); + moment.tz.setDefault(state.user.options.timezone); } - this.trigger(this.state); - }, - - getState() { - return this.state; + store.setState(() => state); }, }; - -export const ConfigStore = createStore(storeConfig); diff --git a/static/app/stores/createLegacyStore.tsx b/static/app/stores/createLegacyStore.tsx new file mode 100644 index 000000000000..278e8e5b3a14 --- /dev/null +++ b/static/app/stores/createLegacyStore.tsx @@ -0,0 +1,54 @@ +import {Store} from '@tanstack/react-store'; + +/** + * Wraps a TanStack Store with the `getState`/`subscribe` shape our stores + * expose, and lets callers replace the state without notifying subscribers. + * + * Reflux stores reset their state silently, and many callers (mostly tests + * resetting stores in `afterEach` while components are still mounted) rely on + * that. Use `resetState` for those writes and `setState` for everything else. + */ +export function createLegacyStore(initialState: T) { + const store = new Store(initialState); + let silent = false; + + const source = { + get: () => store.state, + subscribe: (listener: (state: T) => void) => + store.subscribe(state => { + if (!silent) { + listener(state); + } + }), + }; + + return { + /** + * Pass to `useSelector` from `@tanstack/react-store` + */ + source, + + getState: () => store.state, + + setState: (updater: (prev: T) => T) => store.setState(updater), + + /** + * Returns an unsubscribe function + */ + subscribe(listener: (state: T) => void) { + return source.subscribe(listener).unsubscribe; + }, + + /** + * Replaces the state without notifying subscribers + */ + resetState(state: T) { + silent = true; + try { + store.setState(() => state); + } finally { + silent = false; + } + }, + }; +} diff --git a/static/app/stores/demoWalkthroughStore.tsx b/static/app/stores/demoWalkthroughStore.tsx index e11d2c4d9a40..14203caac482 100644 --- a/static/app/stores/demoWalkthroughStore.tsx +++ b/static/app/stores/demoWalkthroughStore.tsx @@ -1,30 +1,30 @@ -import type {StoreDefinition} from 'reflux'; -import {createStore} from 'reflux'; - +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import {OnboardingTaskKey} from 'sentry/types/onboarding'; -interface DemoWalkthroughStoreDefinition extends StoreDefinition { - activateGuideAnchor(guide: string): void; - get(guide: string): boolean; -} +type State = { + issueGuideAnchor: boolean; + sidebarGuideAnchor: boolean; +}; -const storeConfig: DemoWalkthroughStoreDefinition = { +const store = createLegacyStore({ issueGuideAnchor: false, sidebarGuideAnchor: false, - init() { - // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux - // listeners due to their leaky nature in tests. - }, +}); + +/** + * This store is used to hold local user preferences + * Side-effects (like reading/writing to cookies) are done in associated actionCreators + */ +export const DemoWalkthroughStore = { + ...store, activateGuideAnchor(task: OnboardingTaskKey) { switch (task) { case OnboardingTaskKey.ISSUE_GUIDE: - this.issueGuideAnchor = true; - this.trigger(this.issueGuideAnchor); + store.setState(prev => ({...prev, issueGuideAnchor: true})); break; case OnboardingTaskKey.SIDEBAR_GUIDE: - this.sidebarGuideAnchor = true; - this.trigger(this.sidebarGuideAnchor); + store.setState(prev => ({...prev, sidebarGuideAnchor: true})); break; default: } @@ -33,17 +33,11 @@ const storeConfig: DemoWalkthroughStoreDefinition = { get(guide: string) { switch (guide) { case 'issue': - return this.issueGuideAnchor; + return store.getState().issueGuideAnchor; case 'sidebar': - return this.sidebarGuideAnchor; + return store.getState().sidebarGuideAnchor; default: return false; } }, }; - -/** - * This store is used to hold local user preferences - * Side-effects (like reading/writing to cookies) are done in associated actionCreators - */ -export const DemoWalkthroughStore = createStore(storeConfig); diff --git a/static/app/stores/groupStore.spec.tsx b/static/app/stores/groupStore.spec.tsx index 16dadc2b8be7..92dd4acdd48b 100644 --- a/static/app/stores/groupStore.spec.tsx +++ b/static/app/stores/groupStore.spec.tsx @@ -15,6 +15,9 @@ const g = (id: string, params?: Partial): Group => { }; describe('GroupStore', () => { + let listener: jest.Mock; + let unsubscribe: () => void; + beforeEach(() => { GroupStore.reset(); }); @@ -114,10 +117,12 @@ describe('GroupStore', () => { const stats: Record = {auto: [[1611576000, 10]]}; beforeEach(() => { - jest.spyOn(GroupStore, 'trigger'); + listener = jest.fn(); + unsubscribe = GroupStore.subscribe(listener); GroupStore.items = [g('1'), g('2'), g('3')]; }); afterEach(() => { + unsubscribe(); jest.restoreAllMocks(); }); @@ -134,7 +139,7 @@ describe('GroupStore', () => { const group = GroupStore.getAllItems()[0] as Group; expect(group.stats).toEqual(stats); - expect(GroupStore.trigger).toHaveBeenCalledWith(new Set(['1', '2', '3'])); + expect(listener).toHaveBeenCalled(); }); it('should not change current item ids', () => { @@ -146,7 +151,7 @@ describe('GroupStore', () => { const group1 = GroupStore.getAllItems()[0] as Group; const group2 = GroupStore.getAllItems()[1] as Group; - expect(GroupStore.trigger).toHaveBeenCalledWith(new Set(['2', '3'])); + expect(listener).toHaveBeenCalled(); expect(group1.stats).not.toEqual(stats); expect(group2.stats).toEqual(stats); }); @@ -175,10 +180,12 @@ describe('GroupStore', () => { describe('update methods', () => { beforeEach(() => { - jest.spyOn(GroupStore, 'trigger'); + listener = jest.fn(); + unsubscribe = GroupStore.subscribe(listener); GroupStore.items = [g('1'), g('2'), g('3')]; }); afterEach(() => { + unsubscribe(); jest.restoreAllMocks(); }); @@ -186,8 +193,7 @@ describe('GroupStore', () => { it("should treat undefined itemIds argument as 'all'", () => { GroupStore.onUpdate('1337', undefined, {}); - expect(GroupStore.trigger).toHaveBeenCalledTimes(1); - expect(GroupStore.trigger).toHaveBeenCalledWith(new Set(['1', '2', '3'])); + expect(listener).toHaveBeenCalledTimes(1); }); it('should apply optimistic updates', () => { GroupStore.items = [g('1'), g('2')]; @@ -208,8 +214,7 @@ describe('GroupStore', () => { it("should treat undefined itemIds argument as 'all'", () => { GroupStore.onUpdateSuccess('1337', undefined, {}); - expect(GroupStore.trigger).toHaveBeenCalledTimes(1); - expect(GroupStore.trigger).toHaveBeenCalledWith(new Set(['1', '2', '3'])); + expect(listener).toHaveBeenCalledTimes(1); }); }); @@ -217,8 +222,7 @@ describe('GroupStore', () => { it("should treat undefined itemIds argument as 'all'", () => { GroupStore.onUpdateError('1337', undefined, false); - expect(GroupStore.trigger).toHaveBeenCalledTimes(1); - expect(GroupStore.trigger).toHaveBeenCalledWith(new Set(['1', '2', '3'])); + expect(listener).toHaveBeenCalledTimes(1); }); }); @@ -226,8 +230,7 @@ describe('GroupStore', () => { it("should treat undefined itemIds argument as 'all'", () => { GroupStore.onDeleteSuccess('1337', undefined, {}); - expect(GroupStore.trigger).toHaveBeenCalledTimes(1); - expect(GroupStore.trigger).toHaveBeenCalledWith(new Set(['1', '2', '3'])); + expect(listener).toHaveBeenCalledTimes(1); }); it('should show generic message when itemIds is undefined', () => { @@ -266,8 +269,7 @@ describe('GroupStore', () => { const assignedGroup = g('1', {assignedTo: ActorFixture()}); GroupStore.onAssignToSuccess('1337', '1', assignedGroup); - expect(GroupStore.trigger).toHaveBeenCalledTimes(1); - expect(GroupStore.trigger).toHaveBeenCalledWith(new Set(['1'])); + expect(listener).toHaveBeenCalledTimes(1); expect(GroupStore.items[0]).toEqual(assignedGroup); }); }); diff --git a/static/app/stores/groupStore.tsx b/static/app/stores/groupStore.tsx index 237ed0ddbe36..08a915e230d3 100644 --- a/static/app/stores/groupStore.tsx +++ b/static/app/stores/groupStore.tsx @@ -1,15 +1,12 @@ -import {createStore} from 'reflux'; - import {toast} from '@sentry/scraps/toast'; import {t} from 'sentry/locale'; +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {BaseGroup, Group, GroupStats} from 'sentry/types/group'; import {toArray} from 'sentry/utils/array/toArray'; import {parseApiError} from 'sentry/utils/parseApiError'; import type {RequestError} from 'sentry/utils/requestError/requestError'; -import type {StrictStoreDefinition} from './types'; - function showAlert(msg: string, type: 'error' | 'success') { toast[type](msg, {duration: 4000}); } @@ -25,64 +22,6 @@ type Item = BaseGroup | Group; type ItemIds = string[] | undefined; -interface InternalDefinition { - /** - * Does not include pending changes - * TODO: Remove mutation and replace state with items - */ - items: Item[]; - - pendingChanges: Map; - statuses: Record>; - updateItems: (itemIds: ItemIds) => void; -} - -interface GroupStoreDefinition extends StrictStoreDefinition, InternalDefinition { - add: (items: Item[]) => void; - addStatus: (id: string, status: string) => void; - addToFront: (items: Item[]) => void; - clearStatus: (id: string, status: string) => void; - - get: (id: string) => Readonly | undefined; - getAllItemIds: () => string[]; - getAllItems: () => readonly Item[]; - - hasStatus: (id: string, status: string) => boolean; - init: () => void; - - itemIdsOrAll(itemIds: ItemIds): string[]; - - loadInitialData: (items: Item[]) => void; - - mergeItems: (items: Item[]) => Item[]; - - onAssignTo: (changeId: string, itemId: string, data: any) => void; - onAssignToError: (changeId: string, itemId: string, error: RequestError) => void; - onAssignToSuccess: (changeId: string, itemId: string, response: any) => void; - - onDelete: (changeId: string, itemIds: ItemIds) => void; - onDeleteError: (changeId: string, itemIds: ItemIds, response: RequestError) => void; - onDeleteSuccess: (changeId: string, itemIds: ItemIds, response: any) => void; - - onDiscard: (changeId: string, itemId: string) => void; - onDiscardError: (changeId: string, itemId: string, response: any) => void; - onDiscardSuccess: (changeId: string, itemId: string, response: any) => void; - - onMerge: (changeId: string, itemIds: ItemIds) => void; - onMergeError: (changeId: string, itemIds: ItemIds, response: any) => void; - onMergeSuccess: (changeId: string, itemIds: ItemIds, response: any) => void; - - onPopulateStats: (itemIds: ItemIds, response: GroupStats[]) => void; - - onUpdate: (changeId: string, itemIds: ItemIds, data: any) => void; - onUpdateError: (changeId: string, itemIds: ItemIds, silent: boolean) => void; - onUpdateSuccess: (changeId: string, itemIds: ItemIds, response: Partial) => void; - - remove: (itemIds: ItemIds) => void; - - reset: () => void; -} - function mergePendingChanges( items: readonly Item[], pendingChanges: Map @@ -109,39 +48,41 @@ function mergePendingChanges( ); } -const storeConfig: GroupStoreDefinition = { - pendingChanges: new Map(), - items: [], - state: [], - statuses: {}, +const store = createLegacyStore([]); - init() { - // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux - // listeners due to their leaky nature in tests. +export const GroupStore = { + ...store, + /** + * Does not include pending changes + * TODO: Remove mutation and replace state with items + */ + items: [] as Item[], + pendingChanges: new Map(), + statuses: {} as Record>, - this.reset(); + init() { + GroupStore.reset(); }, reset() { - this.pendingChanges = new Map(); - this.items = []; - this.state = []; - this.statuses = {}; + GroupStore.pendingChanges = new Map(); + GroupStore.items = []; + GroupStore.statuses = {}; + store.resetState([]); }, - loadInitialData(items) { - this.reset(); + loadInitialData(items: Item[]) { + GroupStore.reset(); - const itemIds = items.map(item => item.id); - this.items = [...this.items, ...items]; + GroupStore.items = [...GroupStore.items, ...items]; - this.updateItems(itemIds); + GroupStore.updateItems(); }, - updateItems(itemIds: ItemIds) { - const idSet = new Set(itemIds); - this.state = mergePendingChanges(this.items, this.pendingChanges); - this.trigger(idSet); + updateItems() { + store.setState(() => + mergePendingChanges(GroupStore.items, GroupStore.pendingChanges) + ); }, mergeItems(items: Item[]) { @@ -151,9 +92,9 @@ const storeConfig: GroupStoreDefinition = { }, {}); // Merge these items into the store and return a mapping of any that aren't already in the store - this.items.forEach((item, itemIndex) => { + GroupStore.items.forEach((item, itemIndex) => { if (itemsById[item.id]) { - this.items[itemIndex] = { + GroupStore.items[itemIndex] = { ...item, ...itemsById[item.id], }; @@ -168,86 +109,82 @@ const storeConfig: GroupStoreDefinition = { * Adds the provided items to the end of the list. * If any items already exist, they will merged into the existing item index. */ - add(items) { + add(items: Item[]) { items = toArray(items); - const newItems = this.mergeItems(items); + const newItems = GroupStore.mergeItems(items); - this.items = [...this.items, ...newItems]; + GroupStore.items = [...GroupStore.items, ...newItems]; - this.updateItems(items.map(item => item.id)); + GroupStore.updateItems(); }, /** * Adds the provided items to the front of the list. * If any items already exist, they will be moved to the front in the order provided. */ - addToFront(items) { + addToFront(items: Item[]) { items = toArray(items); const itemMap = items.reduce>((acc, item) => { acc[item.id] = item; return acc; }, {}); - this.items = [...items, ...this.items.filter(item => !itemMap[item.id])]; + GroupStore.items = [...items, ...GroupStore.items.filter(item => !itemMap[item.id])]; - this.updateItems(items.map(item => item.id)); + GroupStore.updateItems(); }, /** * If itemIds is undefined, returns all ids in the store */ - itemIdsOrAll(itemIds: ItemIds) { - return itemIds === undefined ? this.getAllItemIds() : itemIds; + itemIdsOrAll(itemIds: ItemIds): string[] { + return itemIds === undefined ? GroupStore.getAllItemIds() : itemIds; }, - remove(itemIds) { - this.items = this.items.filter(item => !itemIds?.includes(item.id)); + remove(itemIds: ItemIds) { + GroupStore.items = GroupStore.items.filter(item => !itemIds?.includes(item.id)); - this.updateItems(itemIds); + GroupStore.updateItems(); }, - addStatus(id, status) { - if (this.statuses[id] === undefined) { - this.statuses[id] = {}; + addStatus(id: string, status: string) { + if (GroupStore.statuses[id] === undefined) { + GroupStore.statuses[id] = {}; } - this.statuses[id][status] = true; + GroupStore.statuses[id][status] = true; }, - clearStatus(id, status) { - if (this.statuses[id] === undefined) { + clearStatus(id: string, status: string) { + if (GroupStore.statuses[id] === undefined) { return; } - this.statuses[id][status] = false; - }, - - hasStatus(id, status) { - return this.statuses[id]?.[status] || false; + GroupStore.statuses[id][status] = false; }, - get(id) { - return this.getAllItems().find(item => item.id === id); + hasStatus(id: string, status: string): boolean { + return GroupStore.statuses[id]?.[status] || false; }, - getAllItemIds() { - return this.items.map(item => item.id); + get(id: string): Readonly | undefined { + return GroupStore.getAllItems().find(item => item.id === id); }, - getAllItems() { - return this.state; + getAllItemIds(): string[] { + return GroupStore.items.map(item => item.id); }, - getState() { - return this.state; + getAllItems(): readonly Item[] { + return store.getState(); }, - onAssignTo(_changeId, itemId, _data) { - this.addStatus(itemId, 'assignTo'); - this.updateItems([itemId]); + onAssignTo(_changeId: string, itemId: string, _data: any) { + GroupStore.addStatus(itemId, 'assignTo'); + GroupStore.updateItems(); }, // TODO(dcramer): This is not really the best place for this - onAssignToError(_changeId, itemId, error) { - this.clearStatus(itemId, 'assignTo'); + onAssignToError(_changeId: string, itemId: string, error: RequestError) { + GroupStore.clearStatus(itemId, 'assignTo'); const assignedToError = error.responseJSON?.assignedTo; if (Array.isArray(assignedToError) && assignedToError.length > 0) { showAlert(assignedToError[0], 'error'); @@ -260,24 +197,24 @@ const storeConfig: GroupStoreDefinition = { } }, - onAssignToSuccess(_changeId, itemId, response) { - const idx = this.items.findIndex(i => i.id === itemId); + onAssignToSuccess(_changeId: string, itemId: string, response: any) { + const idx = GroupStore.items.findIndex(i => i.id === itemId); if (idx === -1) { return; } - this.items[idx] = {...this.items[idx]!, assignedTo: response.assignedTo}; - this.clearStatus(itemId, 'assignTo'); - this.updateItems([itemId]); + GroupStore.items[idx] = {...GroupStore.items[idx]!, assignedTo: response.assignedTo}; + GroupStore.clearStatus(itemId, 'assignTo'); + GroupStore.updateItems(); }, - onDelete(_changeId, itemIds) { - const ids = this.itemIdsOrAll(itemIds); - ids.forEach(itemId => this.addStatus(itemId, 'delete')); - this.updateItems(ids); + onDelete(_changeId: string, itemIds: ItemIds) { + const ids = GroupStore.itemIdsOrAll(itemIds); + ids.forEach(itemId => GroupStore.addStatus(itemId, 'delete')); + GroupStore.updateItems(); }, - onDeleteError(_changeId, itemIds, response) { + onDeleteError(_changeId: string, itemIds: ItemIds, response: RequestError) { if (response.status === 403) { showAlert(t('You do not have permission to delete issues'), 'error'); } else { @@ -288,12 +225,12 @@ const storeConfig: GroupStoreDefinition = { return; } - itemIds.forEach(itemId => this.clearStatus(itemId, 'delete')); - this.updateItems(itemIds); + itemIds.forEach(itemId => GroupStore.clearStatus(itemId, 'delete')); + GroupStore.updateItems(); }, - onDeleteSuccess(_changeId, itemIds, _response) { - const ids = this.itemIdsOrAll(itemIds); + onDeleteSuccess(_changeId: string, itemIds: ItemIds, _response: any) { + const ids = GroupStore.itemIdsOrAll(itemIds); if (itemIds === undefined) { showAlert(t('Deleted selected issues'), 'success'); @@ -306,60 +243,60 @@ const storeConfig: GroupStoreDefinition = { const itemIdSet = new Set(ids); ids.forEach(itemId => { - delete this.statuses[itemId]; - this.clearStatus(itemId, 'delete'); + delete GroupStore.statuses[itemId]; + GroupStore.clearStatus(itemId, 'delete'); }); - this.items = this.items.filter(item => !itemIdSet.has(item.id)); - this.updateItems(ids); + GroupStore.items = GroupStore.items.filter(item => !itemIdSet.has(item.id)); + GroupStore.updateItems(); }, - onDiscard(_changeId, itemId) { - this.addStatus(itemId, 'discard'); - this.updateItems([itemId]); + onDiscard(_changeId: string, itemId: string) { + GroupStore.addStatus(itemId, 'discard'); + GroupStore.updateItems(); }, - onDiscardError(_changeId, itemId, _response) { - this.clearStatus(itemId, 'discard'); + onDiscardError(_changeId: string, itemId: string, _response: any) { + GroupStore.clearStatus(itemId, 'discard'); showAlert(t('Unable to discard event. Please try again.'), 'error'); - this.updateItems([itemId]); + GroupStore.updateItems(); }, - onDiscardSuccess(_changeId, itemId, _response) { - delete this.statuses[itemId]; - this.clearStatus(itemId, 'discard'); - this.items = this.items.filter(item => item.id !== itemId); + onDiscardSuccess(_changeId: string, itemId: string, _response: any) { + delete GroupStore.statuses[itemId]; + GroupStore.clearStatus(itemId, 'discard'); + GroupStore.items = GroupStore.items.filter(item => item.id !== itemId); showAlert(t('Similar events will be filtered and discarded.'), 'success'); - this.updateItems([itemId]); + GroupStore.updateItems(); }, - onMerge(_changeId, itemIds) { - const ids = this.itemIdsOrAll(itemIds); + onMerge(_changeId: string, itemIds: ItemIds) { + const ids = GroupStore.itemIdsOrAll(itemIds); - ids.forEach(itemId => this.addStatus(itemId, 'merge')); + ids.forEach(itemId => GroupStore.addStatus(itemId, 'merge')); // XXX(billy): Not sure if this is a bug or not but do we need to publish all itemIds? // Seems like we only need to publish parent id - this.updateItems(ids); + GroupStore.updateItems(); }, - onMergeError(_changeId, itemIds, _response) { - const ids = this.itemIdsOrAll(itemIds); + onMergeError(_changeId: string, itemIds: ItemIds, _response: any) { + const ids = GroupStore.itemIdsOrAll(itemIds); - ids.forEach(itemId => this.clearStatus(itemId, 'merge')); + ids.forEach(itemId => GroupStore.clearStatus(itemId, 'merge')); showAlert(t('Unable to merge events. Please try again.'), 'error'); - this.updateItems(ids); + GroupStore.updateItems(); }, - onMergeSuccess(_changeId, itemIds, response) { - const ids = this.itemIdsOrAll(itemIds); // everything on page + onMergeSuccess(_changeId: string, itemIds: ItemIds, response: any) { + const ids = GroupStore.itemIdsOrAll(itemIds); // everything on page - ids.forEach(itemId => this.clearStatus(itemId, 'merge')); + ids.forEach(itemId => GroupStore.clearStatus(itemId, 'merge')); // Remove all but parent id (items were merged into this one) const mergedIdSet = new Set(ids); // Looks like the `PUT /api/0/projects/:orgId/:projectId/issues/` endpoint // actually returns a 204, so there is no `response` body - this.items = this.items.filter( + GroupStore.items = GroupStore.items.filter( item => !mergedIdSet.has(item.id) || item.id === response?.merge?.parent ); @@ -367,66 +304,64 @@ const storeConfig: GroupStoreDefinition = { showAlert(t('Merged %d Issues', ids.length), 'success'); } - this.updateItems(ids); + GroupStore.updateItems(); }, - onUpdate(changeId, itemIds, data) { - const ids = this.itemIdsOrAll(itemIds); + onUpdate(changeId: string, itemIds: ItemIds, data: any) { + const ids = GroupStore.itemIdsOrAll(itemIds); ids.forEach(itemId => { - this.addStatus(itemId, 'update'); + GroupStore.addStatus(itemId, 'update'); }); - this.pendingChanges.set(changeId, {itemIds: ids, data}); + GroupStore.pendingChanges.set(changeId, {itemIds: ids, data}); - this.updateItems(ids); + GroupStore.updateItems(); }, - onUpdateError(changeId, itemIds, failSilently) { - const ids = this.itemIdsOrAll(itemIds); + onUpdateError(changeId: string, itemIds: ItemIds, failSilently: boolean) { + const ids = GroupStore.itemIdsOrAll(itemIds); - this.pendingChanges.delete(changeId); - ids.forEach(itemId => this.clearStatus(itemId, 'update')); + GroupStore.pendingChanges.delete(changeId); + ids.forEach(itemId => GroupStore.clearStatus(itemId, 'update')); if (!failSilently) { showAlert(t('Unable to update events. Please try again.'), 'error'); } - this.updateItems(ids); + GroupStore.updateItems(); }, - onUpdateSuccess(changeId, itemIds, response) { - const ids = this.itemIdsOrAll(itemIds); + onUpdateSuccess(changeId: string, itemIds: ItemIds, response: Partial) { + const ids = GroupStore.itemIdsOrAll(itemIds); - this.items.forEach((item, idx) => { + GroupStore.items.forEach((item, idx) => { if (ids.includes(item.id)) { - this.items[idx] = { + GroupStore.items[idx] = { ...item, ...response, }; - this.clearStatus(item.id, 'update'); + GroupStore.clearStatus(item.id, 'update'); } }); - this.pendingChanges.delete(changeId); - this.updateItems(ids); + GroupStore.pendingChanges.delete(changeId); + GroupStore.updateItems(); }, - onPopulateStats(itemIds, response) { + onPopulateStats(itemIds: ItemIds, response: GroupStats[]) { // Organize stats by id const groupStatsMap = response.reduce>((map, stats) => { map[stats.id] = stats; return map; }, {}); - this.items.forEach((item, idx) => { + GroupStore.items.forEach((item, idx) => { if (itemIds?.includes(item.id)) { - this.items[idx] = { + GroupStore.items[idx] = { ...item, ...groupStatsMap[item.id], }; } }); - this.updateItems(itemIds); + GroupStore.updateItems(); }, }; - -export const GroupStore = createStore(storeConfig); diff --git a/static/app/stores/guideStore.tsx b/static/app/stores/guideStore.tsx index 19ce8a3478f7..98beadba56a8 100644 --- a/static/app/stores/guideStore.tsx +++ b/static/app/stores/guideStore.tsx @@ -1,15 +1,12 @@ -import {createStore} from 'reflux'; - import {getGuidesContent} from 'sentry/components/assistant/getGuidesContent'; import type {Guide, GuidesServerData} from 'sentry/components/assistant/types'; import {getOverride} from 'sentry/overrideRegistry'; import {ConfigStore} from 'sentry/stores/configStore'; +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import {ModalStore} from 'sentry/stores/modalStore'; import type {Organization} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; -import type {StrictStoreDefinition} from './types'; - function guidePrioritySort(a: Guide, b: Guide) { const a_priority = a.priority ?? Number.MAX_SAFE_INTEGER; const b_priority = b.priority ?? Number.MAX_SAFE_INTEGER; @@ -80,76 +77,56 @@ function isForceEnabled() { return window.location.hash === '#assistant'; } -interface GuideStoreDefinition extends StrictStoreDefinition { - closeGuide(dismissed?: boolean): void; - - fetchSucceeded(data: GuidesServerData): void; - modalStoreListener: null | ReturnType; - nextStep(): void; - onURLChange(): void; - recordCue(guide: string): void; - registerAnchor(target: string): void; - setActiveOrganization(data: Organization): void; - setForceHide(forceHide: boolean): void; - teardown(): void; - unregisterAnchor(target: string): void; - updateCurrentGuide(dismissed?: boolean): void; - updatePrevGuide(nextGuide: Guide | null): void; +const store = createLegacyStore({...defaultState}); + +let unsubscribeModalStore: (() => void) | null = null; + +function onURLChange() { + store.setState(prev => ({...prev, forceShow: isForceEnabled()})); + GuideStore.updateCurrentGuide(); } -const storeConfig: GuideStoreDefinition = { - state: {...defaultState}, - modalStoreListener: null, +export const GuideStore = { + ...store, - init() { - // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux - // listeners due to their leaky nature in tests. + /** + * @deprecated Use `getState()` + */ + get state() { + return store.getState(); + }, - this.state = {...defaultState, forceShow: isForceEnabled()}; + init() { + store.resetState({...defaultState, forceShow: isForceEnabled()}); - window.addEventListener('load', this.onURLChange, false); + window.addEventListener('load', onURLChange, false); // Guides will show above modals, but are not interactable because // of the focus trap, so we force them to be hidden while a modal is open. - this.modalStoreListener = ModalStore.listen(() => { - const isOpen = typeof ModalStore.getState().renderer === 'function'; - - if (isOpen) { - this.setForceHide(true); - } else { - this.setForceHide(false); - } - }, undefined); + unsubscribeModalStore = ModalStore.subscribe(modalState => { + GuideStore.setForceHide(typeof modalState.renderer === 'function'); + }); }, teardown() { - window.removeEventListener('load', this.onURLChange); - - if (this.modalStoreListener) { - this.modalStoreListener(); - } - }, - - getState() { - return this.state; + window.removeEventListener('load', onURLChange); + unsubscribeModalStore?.(); + unsubscribeModalStore = null; }, - onURLChange() { - this.state = {...this.state, forceShow: isForceEnabled()}; - this.updateCurrentGuide(); - }, + onURLChange, setActiveOrganization(data: Organization) { - this.state = { - ...this.state, + store.setState(prev => ({ + ...prev, orgId: data ? data.id : null, orgSlug: data ? data.slug : null, organization: data ? data : null, - }; - this.updateCurrentGuide(); + })); + GuideStore.updateCurrentGuide(); }, - fetchSucceeded(data) { + fetchSucceeded(data: GuidesServerData) { // It's possible we can get empty responses (seems to be Firefox specific) // Do nothing if `data` is empty // also, temporarily check data is in the correct format from the updated @@ -171,12 +148,12 @@ const storeConfig: GuideStoreDefinition = { return acc; }, []); - this.state = {...this.state, guides}; - this.updateCurrentGuide(); + store.setState(prev => ({...prev, guides})); + GuideStore.updateCurrentGuide(); }, closeGuide(dismissed?: boolean) { - const {currentGuide, guides} = this.state; + const {currentGuide, guides} = store.getState(); const newGuides = guides.map(guide => { // update the current guide seen to true or all guides @@ -193,51 +170,49 @@ const storeConfig: GuideStoreDefinition = { return guide; }); - this.state = {...this.state, guides: newGuides, forceShow: false}; - this.updateCurrentGuide(dismissed); + store.setState(prev => ({...prev, guides: newGuides, forceShow: false})); + GuideStore.updateCurrentGuide(dismissed); }, nextStep() { - this.state = {...this.state, currentStep: this.state.currentStep + 1}; - this.trigger(this.state); + store.setState(prev => ({...prev, currentStep: store.getState().currentStep + 1})); }, - registerAnchor(target) { - this.state.anchors.add(target); - this.updateCurrentGuide(); + registerAnchor(target: string) { + store.getState().anchors.add(target); + GuideStore.updateCurrentGuide(); }, - unregisterAnchor(target) { - this.state.anchors.delete(target); - this.updateCurrentGuide(); + unregisterAnchor(target: string) { + store.getState().anchors.delete(target); + GuideStore.updateCurrentGuide(); }, - setForceHide(forceHide) { - this.state = {...this.state, forceHide}; - this.trigger(this.state); + setForceHide(forceHide: boolean) { + store.setState(prev => ({...prev, forceHide})); }, - recordCue(guide) { + recordCue(guide: string) { const user = ConfigStore.get('user'); if (!user) { return; } trackAnalytics('assistant.guide_cued', { - organization: this.state.orgId, + organization: store.getState().orgId, guide, }); }, - updatePrevGuide(nextGuide) { - const {prevGuide} = this.state; + updatePrevGuide(nextGuide: Guide | null) { + const {prevGuide} = store.getState(); if (!nextGuide) { return; } if (prevGuide?.guide !== nextGuide.guide) { - this.recordCue(nextGuide.guide); - this.state = {...this.state, prevGuide: nextGuide}; + GuideStore.recordCue(nextGuide.guide); + store.setState(prev => ({...prev, prevGuide: nextGuide})); } }, @@ -250,7 +225,7 @@ const storeConfig: GuideStoreDefinition = { * - Otherwise show the guide */ updateCurrentGuide(dismissed?: boolean) { - const {anchors, guides, forceShow} = this.state; + const {anchors, guides, forceShow} = store.getState(); let guideOptions = guides .sort(guidePrioritySort) @@ -290,16 +265,17 @@ const storeConfig: GuideStoreDefinition = { } : null; - this.updatePrevGuide(nextGuide); - const currentStep = - this.state.currentGuide && this.state.currentGuide.guide === nextGuide?.guide - ? this.state.currentStep - : 0; - this.state = {...this.state, currentGuide: nextGuide, currentStep}; + GuideStore.updatePrevGuide(nextGuide); + const {currentGuide, currentStep} = store.getState(); + store.setState(prev => ({ + ...prev, + currentGuide: nextGuide, + currentStep: + currentGuide && currentGuide.guide === nextGuide?.guide ? currentStep : 0, + })); - this.trigger(this.state); getOverride('callback:on-guide-update')?.(nextGuide, {dismissed}); }, }; -export const GuideStore = createStore(storeConfig); +GuideStore.init(); diff --git a/static/app/stores/modalStore.tsx b/static/app/stores/modalStore.tsx index 63debaa49b98..7b67780e9584 100644 --- a/static/app/stores/modalStore.tsx +++ b/static/app/stores/modalStore.tsx @@ -1,9 +1,7 @@ import type {FocusTrap} from 'focus-trap'; -import {createStore} from 'reflux'; import type {ModalOptions, ModalRenderProps} from 'sentry/actionCreators/modal'; - -import type {StrictStoreDefinition} from './types'; +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; type Renderer = (renderProps: ModalRenderProps) => React.ReactNode; @@ -14,53 +12,43 @@ type State = { triggerElement?: HTMLElement | null; }; -interface ModalStoreDefinition extends StrictStoreDefinition { - closeModal(): void; - init(): void; - openModal(renderer: Renderer, options: ModalOptions): void; - reset(): void; - setFocusTrap(focusTrap: FocusTrap): void; -} - -const storeConfig: ModalStoreDefinition = { - state: {renderer: null, options: {}}, - init() { - // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux - // listeners due to their leaky nature in tests. +const store = createLegacyStore({renderer: null, options: {}}); - this.reset(); - }, +export const ModalStore = { + ...store, - getState() { - return this.state; + init() { + ModalStore.reset(); }, reset() { - this.state = { + store.resetState({ renderer: null, options: {}, - focusTrap: this.state.focusTrap, - }; + focusTrap: store.getState().focusTrap, + }); }, closeModal() { - this.reset(); - this.trigger(this.state); + store.setState(() => ({ + renderer: null, + options: {}, + focusTrap: store.getState().focusTrap, + })); }, openModal(renderer: Renderer, options: ModalOptions) { const triggerElement = document.activeElement instanceof HTMLElement ? document.activeElement : null; - this.state = {renderer, options, focusTrap: this.state.focusTrap, triggerElement}; - this.trigger(this.state); + store.setState(() => ({ + renderer, + options, + focusTrap: store.getState().focusTrap, + triggerElement, + })); }, setFocusTrap(focusTrap: FocusTrap) { - this.state = { - ...this.state, - focusTrap, - }; + store.resetState({...store.getState(), focusTrap}); }, }; - -export const ModalStore = createStore(storeConfig); diff --git a/static/app/stores/onboardingDrawerStore.tsx b/static/app/stores/onboardingDrawerStore.tsx index 02c0157e16db..b63cf3140b05 100644 --- a/static/app/stores/onboardingDrawerStore.tsx +++ b/static/app/stores/onboardingDrawerStore.tsx @@ -1,16 +1,7 @@ -import {createStore} from 'reflux'; - -import type {StrictStoreDefinition} from './types'; +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; type ActivePanelType = Readonly; -interface OnboardingDrawerStoreDefinition extends StrictStoreDefinition { - close(): void; - - open(panel: OnboardingDrawerKey): void; - toggle(panel: OnboardingDrawerKey): void; -} - export enum OnboardingDrawerKey { BROADCASTS = 'broadcasts', ONBOARDING_WIZARD = 'todos', @@ -22,39 +13,30 @@ export enum OnboardingDrawerKey { FEATURE_FLAG_ONBOARDING = 'flag_onboarding', } -const storeConfig: OnboardingDrawerStoreDefinition = { - state: '', +const store = createLegacyStore(''); - init() { - // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux - // listeners due to their leaky nature in tests. - }, +/** + * This store is used to hold local user preferences + * Side-effects (like reading/writing to cookies) are done in associated actionCreators + */ +export const OnboardingDrawerStore = { + ...store, + + init() {}, open(panel: OnboardingDrawerKey) { - this.state = panel; - this.trigger(this.state); + store.setState(() => panel); }, toggle(panel: OnboardingDrawerKey) { - if (this.state === panel) { - this.close(); + if (store.getState() === panel) { + OnboardingDrawerStore.close(); } else { - this.open(panel); + OnboardingDrawerStore.open(panel); } }, close() { - this.state = ''; - this.trigger(this.state); - }, - - getState() { - return this.state; + store.setState(() => ''); }, }; - -/** - * This store is used to hold local user preferences - * Side-effects (like reading/writing to cookies) are done in associated actionCreators - */ -export const OnboardingDrawerStore = createStore(storeConfig); diff --git a/static/app/stores/organizationStore.tsx b/static/app/stores/organizationStore.tsx index 822f5ad0866f..96a1911e3fae 100644 --- a/static/app/stores/organizationStore.tsx +++ b/static/app/stores/organizationStore.tsx @@ -1,12 +1,9 @@ -import {createStore} from 'reflux'; - import {ORGANIZATION_FETCH_ERROR_TYPES} from 'sentry/constants'; +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {Organization} from 'sentry/types/organization'; import type {RequestError} from 'sentry/utils/requestError/requestError'; import {callSetOrganizationCallback} from 'sentry/utils/routeAnalytics/setOrganizationCallback'; -import type {StrictStoreDefinition} from './types'; - type State = { dirty: boolean; loading: boolean; @@ -15,58 +12,47 @@ type State = { errorType?: string | null; }; -interface OrganizationStoreDefinition extends StrictStoreDefinition { - get(): State; - onFetchOrgError(err: RequestError): void; - onUpdate(org: Organization, options?: {replace: true}): void; - onUpdate(org: Partial, options?: {replace?: false}): void; - reset(): void; - setNoOrganization(): void; -} +const initialState: State = { + dirty: false, + loading: true, + organization: null, + error: null, + errorType: null, +}; + +const store = createLegacyStore(initialState); -const storeConfig: OrganizationStoreDefinition = { - state: { +function onUpdate(org: Organization, options?: {replace: true}): void; +function onUpdate(org: Partial, options?: {replace?: false}): void; +function onUpdate(updatedOrg: Partial, {replace = false} = {}) { + const organization = ( + replace ? updatedOrg : {...store.getState().organization, ...updatedOrg} + ) as Organization; + store.setState(() => ({ + loading: false, dirty: false, - loading: true, - organization: null, - error: null, errorType: null, - }, - init() { - // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux - // listeners due to their leaky nature in tests. + error: null, + organization, + })); + + callSetOrganizationCallback(organization); +} - this.reset(); +export const OrganizationStore = { + ...store, + + init() { + OrganizationStore.reset(); }, reset() { - this.state = { - dirty: false, - loading: true, - organization: null, - error: null, - errorType: null, - }; - this.trigger(this.get()); + store.setState(() => initialState); }, - onUpdate(updatedOrg: Organization, {replace = false} = {}) { - const organization = replace - ? updatedOrg - : {...this.state.organization, ...updatedOrg}; - this.state = { - loading: false, - dirty: false, - errorType: null, - error: null, - organization, - }; - this.trigger(this.get()); + onUpdate, - callSetOrganizationCallback(organization); - }, - - onFetchOrgError(err) { + onFetchOrgError(err: RequestError) { let errorType: State['errorType'] = null; switch (err?.status) { @@ -78,34 +64,26 @@ const storeConfig: OrganizationStoreDefinition = { break; default: } - this.state = { + store.setState(() => ({ errorType, dirty: false, error: err, loading: false, organization: null, - }; - this.trigger(this.get()); + })); }, setNoOrganization() { - this.state = { - ...this.state, + store.setState(prev => ({ + ...prev, organization: null, errorType: ORGANIZATION_FETCH_ERROR_TYPES.NO_ORGS, loading: false, dirty: false, - }; - this.trigger(this.get()); + })); }, get() { - return this.state; - }, - - getState() { - return this.state; + return store.getState(); }, }; - -export const OrganizationStore = createStore(storeConfig); diff --git a/static/app/stores/organizationsStore.tsx b/static/app/stores/organizationsStore.tsx index 376142669bc0..beac6a831129 100644 --- a/static/app/stores/organizationsStore.tsx +++ b/static/app/stores/organizationsStore.tsx @@ -1,38 +1,23 @@ -import {createStore} from 'reflux'; - +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {OrganizationSummary} from 'sentry/types/organization'; -import type {StrictStoreDefinition} from './types'; - interface State { loaded: boolean; organizations: OrganizationSummary[]; } -interface OrganizationsStoreDefinition extends StrictStoreDefinition { - addOrReplace(item: OrganizationSummary): void; - get(slug: string): OrganizationSummary | undefined; - getAll(): OrganizationSummary[]; - load(items: OrganizationSummary[]): void; - onChangeSlug(prev: OrganizationSummary, next: Partial): void; - onRemoveSuccess(slug: string): void; - onUpdate(org: Partial): void; - remove(slug: string): void; -} +const store = createLegacyStore({organizations: [], loaded: false}); -const storeConfig: OrganizationsStoreDefinition = { - state: {organizations: [], loaded: false}, +export const OrganizationsStore = { + ...store, init() { - // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux - // listeners due to their leaky nature in tests. - - this.state = {organizations: [], loaded: false}; + store.resetState({organizations: [], loaded: false}); }, - onUpdate(org) { + onUpdate(org: Partial) { let match = false; - const newOrgs = [...this.state.organizations]; + const newOrgs = [...store.getState().organizations]; newOrgs.forEach((existing, idx) => { if (existing.id === org.id) { newOrgs[idx] = {...existing, ...org}; @@ -44,46 +29,40 @@ const storeConfig: OrganizationsStoreDefinition = { 'Cannot update an organization that is not in the OrganizationsStore' ); } - this.state = {...this.state, organizations: newOrgs}; - this.trigger(newOrgs); + store.setState(prev => ({...prev, organizations: newOrgs})); }, - onChangeSlug(prev, next) { + onChangeSlug(prev: OrganizationSummary, next: Partial) { if (prev.slug === next.slug) { return; } - this.remove(prev.slug); - this.addOrReplace({...prev, ...next}); + OrganizationsStore.remove(prev.slug); + OrganizationsStore.addOrReplace({...prev, ...next}); }, - onRemoveSuccess(slug) { - this.remove(slug); + onRemoveSuccess(slug: string) { + OrganizationsStore.remove(slug); }, - get(slug) { - return this.state.organizations.find(item => item.slug === slug); + get(slug: string) { + return store.getState().organizations.find(item => item.slug === slug); }, getAll() { - return this.state.organizations; + return store.getState().organizations; }, - getState() { - return this.state; + remove(slug: string) { + store.setState(prev => ({ + ...prev, + organizations: store.getState().organizations.filter(item => slug !== item.slug), + })); }, - remove(slug) { - this.state = { - ...this.state, - organizations: this.state.organizations.filter(item => slug !== item.slug), - }; - this.trigger(this.state.organizations); - }, - - addOrReplace(item) { + addOrReplace(item: OrganizationSummary) { let match = false; - const newOrgs = [...this.state.organizations]; + const newOrgs = [...store.getState().organizations]; newOrgs.forEach((existing, idx) => { if (existing.id === item.id) { newOrgs[idx] = {...existing, ...item}; @@ -93,15 +72,10 @@ const storeConfig: OrganizationsStoreDefinition = { if (!match) { newOrgs.push(item); } - this.state = {...this.state, organizations: newOrgs}; - this.trigger(newOrgs); + store.setState(prev => ({...prev, organizations: newOrgs})); }, load(items: OrganizationSummary[]) { - const newOrgs = [...items]; - this.state = {organizations: newOrgs, loaded: true}; - this.trigger(newOrgs); + store.setState(() => ({organizations: [...items], loaded: true})); }, }; - -export const OrganizationsStore = createStore(storeConfig); diff --git a/static/app/stores/projectsStore.tsx b/static/app/stores/projectsStore.tsx index ccde47a6cf73..c1cd14abc5eb 100644 --- a/static/app/stores/projectsStore.tsx +++ b/static/app/stores/projectsStore.tsx @@ -1,124 +1,92 @@ -import {createStore} from 'reflux'; - -import {Client} from 'sentry/api'; import {clearQueryCache} from 'sentry/appQueryClient'; +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {Team} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; -import type {StrictStoreDefinition} from './types'; - type State = { loading: boolean; projects: Project[]; }; -/** - * Attributes that need typing but aren't part of the external interface, - */ -type InternalDefinition = { - api: Client; - removeTeamFromProject(teamSlug: string, project: Project): void; -}; +const store = createLegacyStore({projects: [], loading: true}); -interface ProjectsStoreDefinition - extends InternalDefinition, StrictStoreDefinition { - getById(id?: string): Project | undefined; - getBySlug(slug?: string): Project | undefined; - isLoading(): boolean; - loadInitialData(projects: Project[]): void; - onAddTeam(team: Team, projectSlug: string): void; - onChangeSlug(prevSlug: string, newSlug: string): void; - onCreateSuccess(project: Project): void; - onDeleteProject(projectSlug: string): void; - onDeleteTeam(slug: string): void; - onRemoveTeam(teamSlug: string, projectSlug: string): void; - onUpdateSuccess(data: Partial): void; - reset(): void; +function setProjects(projects: Project[]) { + store.setState(prev => ({...prev, projects})); } -const storeConfig: ProjectsStoreDefinition = { - api: new Client(), - state: { - projects: [], - loading: true, - }, +// Internal helper, the caller clears the query cache +function removeTeamFromProject(teamSlug: string, project: Project) { + const newTeams = project.teams.filter(({slug}) => slug !== teamSlug); + const newProject = {...project, teams: newTeams}; + setProjects(store.getState().projects.map(p => (p.id === project.id ? newProject : p))); + clearQueryCache(); +} - init() { - // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux - // listeners due to their leaky nature in tests. +export const ProjectsStore = { + ...store, - this.reset(); + init() { + ProjectsStore.reset(); }, reset() { - this.state = { - projects: [], - loading: true, - }; + store.resetState({projects: [], loading: true}); }, loadInitialData(items: Project[]) { - this.state = { - projects: items.toSorted((a: any, b: any) => a.slug.localeCompare(b.slug)), + store.setState(() => ({ + projects: items.toSorted((a, b) => a.slug.localeCompare(b.slug)), loading: false, - }; - - this.trigger(new Set(items.map(x => x.id))); + })); }, onChangeSlug(prevSlug: string, newSlug: string) { - const prevProject = this.getBySlug(prevSlug); + const prevProject = ProjectsStore.getBySlug(prevSlug); if (!prevProject) { return; } const newProject = {...prevProject, slug: newSlug}; - const newProjects = this.state.projects - .map(project => (project.slug === prevSlug ? newProject : project)) - .toSorted((a: any, b: any) => a.slug.localeCompare(b.slug)); - this.state = {...this.state, projects: newProjects}; - - this.trigger(new Set([prevProject.id])); + setProjects( + store + .getState() + .projects.map(project => (project.slug === prevSlug ? newProject : project)) + .toSorted((a, b) => a.slug.localeCompare(b.slug)) + ); clearQueryCache(); }, onCreateSuccess(project: Project) { - const newProjects = this.state.projects - .concat([project]) - .sort((a, b) => a.slug.localeCompare(b.slug)); - this.state = {...this.state, projects: newProjects}; - + setProjects( + store + .getState() + .projects.concat([project]) + .sort((a, b) => a.slug.localeCompare(b.slug)) + ); clearQueryCache(); - - this.trigger(new Set([project.id])); }, onUpdateSuccess(data: Partial) { - const project = this.getById(data.id); + const project = ProjectsStore.getById(data.id); if (!project) { return; } const newProject = {...project, ...data}; - const newProjects = this.state.projects.map(p => - p.id === project.id ? newProject : p + setProjects( + store.getState().projects.map(p => (p.id === project.id ? newProject : p)) ); - this.state = {...this.state, projects: newProjects}; - - this.trigger(new Set([data.id])); clearQueryCache(); }, onDeleteProject(projectSlug: string) { - const project = this.getBySlug(projectSlug); + const project = ProjectsStore.getBySlug(projectSlug); if (!project) { return; } - const newProjects = this.state.projects.filter(p => p.id !== project.id); - this.state = {...this.state, projects: newProjects}; - this.trigger(new Set([project.id])); + setProjects(store.getState().projects.filter(p => p.id !== project.id)); clearQueryCache(); }, @@ -129,31 +97,27 @@ const storeConfig: ProjectsStoreDefinition = { */ onDeleteTeam(teamSlug: string) { // Look for team in all projects - const projects = this.state.projects.filter(({teams}) => - teams.find(({slug}) => slug === teamSlug) - ); + const projects = store + .getState() + .projects.filter(({teams}) => teams.find(({slug}) => slug === teamSlug)); - projects.forEach(project => this.removeTeamFromProject(teamSlug, project)); - - const affectedProjectIds = projects.map(project => project.id); - this.trigger(new Set(affectedProjectIds)); + projects.forEach(project => removeTeamFromProject(teamSlug, project)); clearQueryCache(); }, onRemoveTeam(teamSlug: string, projectSlug: string) { - const project = this.getBySlug(projectSlug); + const project = ProjectsStore.getBySlug(projectSlug); if (!project) { return; } - this.removeTeamFromProject(teamSlug, project); - this.trigger(new Set([project.id])); + removeTeamFromProject(teamSlug, project); clearQueryCache(); }, onAddTeam(team: Team, projectSlug: string) { - const project = this.getBySlug(projectSlug); + const project = ProjectsStore.getBySlug(projectSlug); // Don't do anything if we can't find a project if (!project) { @@ -161,41 +125,21 @@ const storeConfig: ProjectsStoreDefinition = { } const newProject = {...project, teams: [...project.teams, team]}; - const newProjects = this.state.projects.map(p => - p.id === project.id ? newProject : p - ); - this.state = {...this.state, projects: newProjects}; - - this.trigger(new Set([project.id])); - clearQueryCache(); - }, - - // Internal method, does not trigger - removeTeamFromProject(teamSlug: string, project: Project) { - const newTeams = project.teams.filter(({slug}) => slug !== teamSlug); - const newProject = {...project, teams: newTeams}; - const newProjects = this.state.projects.map(p => - p.id === project.id ? newProject : p + setProjects( + store.getState().projects.map(p => (p.id === project.id ? newProject : p)) ); - this.state = {...this.state, projects: newProjects}; clearQueryCache(); }, isLoading() { - return this.state.loading; + return store.getState().loading; }, - getById(id) { - return this.state.projects.find(project => project.id === id); + getById(id?: string) { + return store.getState().projects.find(project => project.id === id); }, - getBySlug(slug) { - return this.state.projects.find(project => project.slug === slug); - }, - - getState() { - return this.state; + getBySlug(slug?: string) { + return store.getState().projects.find(project => project.slug === slug); }, }; - -export const ProjectsStore = createStore(storeConfig); diff --git a/static/app/stores/tagStore.spec.tsx b/static/app/stores/tagStore.spec.tsx index 5de801cd1127..05bea2339f84 100644 --- a/static/app/stores/tagStore.spec.tsx +++ b/static/app/stores/tagStore.spec.tsx @@ -9,7 +9,8 @@ describe('TagStore', () => { describe('loadTagsSuccess()', () => { it('should add a new tag with empty values and trigger the new addition', () => { - jest.spyOn(TagStore, 'trigger'); + const listener = jest.fn(); + const unsubscribe = TagStore.subscribe(listener); TagStore.loadTagsSuccess([ { @@ -31,7 +32,8 @@ describe('TagStore', () => { values: [], }); - expect(TagStore.trigger).toHaveBeenCalledTimes(1); + expect(listener).toHaveBeenCalledTimes(1); + unsubscribe(); }); }); diff --git a/static/app/stores/tagStore.tsx b/static/app/stores/tagStore.tsx index 7bd7e2cac943..e3ff225fc08b 100644 --- a/static/app/stores/tagStore.tsx +++ b/static/app/stores/tagStore.tsx @@ -1,35 +1,22 @@ -import {createStore} from 'reflux'; - +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {Tag, TagCollection} from 'sentry/types/group'; -import type {StrictStoreDefinition} from './types'; - -interface TagStoreDefinition extends StrictStoreDefinition { - loadTagsSuccess(data: Tag[]): void; - reset(): void; -} +const store = createLegacyStore({}); -const storeConfig: TagStoreDefinition = { - state: {}, +export const TagStore = { + ...store, init() { - // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux - // listeners due to their leaky nature in tests. - this.state = {}; - }, - - getState() { - return this.state; + store.resetState({}); }, reset() { - this.state = {}; - this.trigger(this.state); + store.setState(() => ({})); }, - loadTagsSuccess(data) { + loadTagsSuccess(data: Tag[]) { // Note: We could probably stop cloning the data here and just - // assign to this.state directly, but there is a change someone may + // assign to the state directly, but there is a change someone may // be relying on referential equality somewhere in the codebase and // we dont want to risk breaking that. const newState: TagCollection = {}; @@ -41,10 +28,11 @@ const storeConfig: TagStoreDefinition = { }; } + const previousState = store.getState(); // We will iterate through the previous tags in reverse so that previously // added tags are carried over first. We rely on browser implementation // of Object.keys() to return keys in insertion order. - const previousTagKeys = Object.keys(this.state); + const previousTagKeys = Object.keys(previousState); const MAX_STORE_SIZE = 2000; // We will carry over the previous tags until we reach the max store size @@ -63,13 +51,10 @@ const storeConfig: TagStoreDefinition = { continue; } // Else override the tag with the previous tag - newState[tagKey] = this.state[tagKey]!; + newState[tagKey] = previousState[tagKey]!; carriedOver++; } - this.state = newState; - this.trigger(newState); + store.setState(() => newState); }, }; - -export const TagStore = createStore(storeConfig); diff --git a/static/app/stores/teamStore.tsx b/static/app/stores/teamStore.tsx index 1942875fb2f1..9c4522e4dfad 100644 --- a/static/app/stores/teamStore.tsx +++ b/static/app/stores/teamStore.tsx @@ -1,10 +1,8 @@ -import {createStore} from 'reflux'; - +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {Team} from 'sentry/types/organization'; import {defined} from 'sentry/utils/defined'; import {ProjectsStore} from './projectsStore'; -import type {StrictStoreDefinition} from './types'; type State = { cursor: string | null; @@ -14,91 +12,79 @@ type State = { teams: Team[]; }; -interface TeamStoreDefinition extends StrictStoreDefinition { - getAll(): Team[]; - getById(id: string): Team | null; - getBySlug(slug: string): Team | null; - initialized: boolean; - loadInitialData(items: Team[], hasMore?: boolean | null, cursor?: string | null): void; - loadUserTeams(userTeams: Team[]): void; - onCreateSuccess(team: Team): void; - onRemoveSuccess(slug: string): void; - onUpdateSuccess(itemId: string, response: Team): void; - reset(): void; - setTeams(teams: Team[], hasMore?: boolean | null, cursor?: string | null): void; - updateTeams(teams: Team[]): Team[]; +const initialState: State = { + teams: [], + loadedUserTeams: false, + loading: true, + hasMore: null, + cursor: null, +}; + +const store = createLegacyStore(initialState); + +function updateTeams(teams: Team[]) { + const teamIdMap = store + .getState() + .teams.reduce((acc: Record, team: Team) => { + acc[team.id] = team; + return acc; + }, {}); + + // Replace or insert new user teams + teams.reduce((acc: Record, userTeam: Team) => { + acc[userTeam.id] = userTeam; + return acc; + }, teamIdMap); + + return Object.values(teamIdMap).sort((a, b) => a.slug.localeCompare(b.slug)); } -const teamStoreConfig: TeamStoreDefinition = { +export const TeamStore = { + ...store, initialized: false, - state: { - teams: [], - loadedUserTeams: false, - loading: true, - hasMore: null, - cursor: null, - }, init() { - // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux - // listeners due to their leaky nature in tests. - - this.reset(); + TeamStore.reset(); }, reset() { - this.state = { - teams: [], - loadedUserTeams: false, - loading: true, - hasMore: null, - cursor: null, - }; + store.resetState(initialState); }, - setTeams(teams, hasMore, cursor) { - this.initialized = true; - this.state = { + setTeams(teams: Team[], hasMore?: boolean | null, cursor?: string | null) { + TeamStore.initialized = true; + const state = store.getState(); + store.setState(() => ({ teams, - loadedUserTeams: defined(hasMore) ? !hasMore : this.state.loadedUserTeams, + loadedUserTeams: defined(hasMore) ? !hasMore : state.loadedUserTeams, loading: false, - hasMore: hasMore ?? this.state.hasMore, - cursor: cursor ?? this.state.cursor, - }; - this.trigger(new Set(teams.map(team => team.id))); + hasMore: hasMore ?? state.hasMore, + cursor: cursor ?? state.cursor, + })); }, - loadInitialData(items, hasMore, cursor) { - const teams = this.updateTeams(items); - this.setTeams(teams, hasMore, cursor); + loadInitialData(items: Team[], hasMore?: boolean | null, cursor?: string | null) { + TeamStore.setTeams(updateTeams(items), hasMore, cursor); }, loadUserTeams(userTeams: Team[]) { - const teams = this.updateTeams(userTeams); - - this.state = { - ...this.state, + store.setState(prev => ({ + ...prev, loadedUserTeams: true, - teams, - }; - - this.trigger(new Set(teams.map(team => team.id))); + teams: updateTeams(userTeams), + })); }, - onUpdateSuccess(itemId, response) { + onUpdateSuccess(itemId: string, response: Team) { if (!response) { return; } - const item = this.getBySlug(itemId); + const {teams} = store.getState(); + const item = TeamStore.getBySlug(itemId); if (!item) { - this.state = { - ...this.state, - teams: [...this.state.teams, response], - }; - - this.trigger(new Set([itemId])); + store.setState(prev => ({...prev, teams: [...teams, response]})); return; } @@ -107,63 +93,40 @@ const teamStoreConfig: TeamStoreDefinition = { // components use stores correctly. To be safe reload browser :(( if (response.slug !== itemId) { // Replace the team - const teams = [...this.state.teams.filter(({slug}) => slug !== itemId), response]; - - this.state = {...this.state, teams}; - this.trigger(new Set([response.slug])); + store.setState(prev => ({ + ...prev, + teams: [...teams.filter(({slug}) => slug !== itemId), response], + })); return; } - const newTeams = [...this.state.teams]; + const newTeams = [...teams]; const index = newTeams.findIndex(team => team.slug === response.slug); newTeams[index] = response; - this.state = {...this.state, teams: newTeams}; - this.trigger(new Set([itemId])); + store.setState(prev => ({...prev, teams: newTeams})); }, onRemoveSuccess(slug: string) { - const teams = this.state.teams.filter(team => team.slug !== slug); - this.setTeams(teams); + TeamStore.setTeams(store.getState().teams.filter(team => team.slug !== slug)); ProjectsStore.onDeleteTeam(slug); }, onCreateSuccess(team: Team) { - this.loadInitialData([team]); - }, - - getState() { - return this.state; + TeamStore.loadInitialData([team]); }, getById(id: string) { - const {teams} = this.state; - return teams.find(item => item.id.toString() === id.toString()) || null; + return ( + store.getState().teams.find(item => item.id.toString() === id.toString()) || null + ); }, getBySlug(slug: string) { - const {teams} = this.state; - return teams.find(item => item.slug === slug) || null; + return store.getState().teams.find(item => item.slug === slug) || null; }, getAll() { - return this.state.teams; - }, - - updateTeams(teams: Team[]) { - const teamIdMap = this.state.teams.reduce((acc: Record, team: Team) => { - acc[team.id] = team; - return acc; - }, {}); - - // Replace or insert new user teams - teams.reduce((acc: Record, userTeam: Team) => { - acc[userTeam.id] = userTeam; - return acc; - }, teamIdMap); - - return Object.values(teamIdMap).sort((a, b) => a.slug.localeCompare(b.slug)); + return store.getState().teams; }, }; - -export const TeamStore = createStore(teamStoreConfig); diff --git a/static/app/stores/types.tsx b/static/app/stores/types.tsx deleted file mode 100644 index 82786a87b0a4..000000000000 --- a/static/app/stores/types.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/** - * All stores implementing this interface have a common getState which returns - * the stores state. - * - * When a store implements this it becomes usable with the `useLegacyStore` hook. - * - * Does not have the `[key: string]: any;` index signature that `Reflux.StoreDefinition` has. - */ -export interface StrictStoreDefinition { - /** - * Returns the current state represented within the store - */ - getState(): Readonly; - init(): void; - state: Readonly; - /** - * Trigger is not defined by the store definition, but is added by Reflux - * We could try to type this better, but would need to update all .listen calls - */ - trigger?: any; -} diff --git a/static/app/stores/useLegacyStore.tsx b/static/app/stores/useLegacyStore.tsx index b94e0d805d9a..d02ca93e7e71 100644 --- a/static/app/stores/useLegacyStore.tsx +++ b/static/app/stores/useLegacyStore.tsx @@ -1,29 +1,18 @@ -import {useCallback, useSyncExternalStore} from 'react'; -import type {Store} from 'reflux'; +import {useSelector} from '@tanstack/react-store'; -interface LegacyStoreShape extends Pick { - getState(): any; -} +import type {createLegacyStore} from 'sentry/stores/createLegacyStore'; + +type LegacyStore = Pick>, 'source' | 'getState'>; /** - * Returns the state of a reflux store. Automatically unsubscribes when destroyed + * Returns the state of a store and re-renders when it changes. * * ```tsx * const teams = useLegacyStore(TeamStore); * ``` - * - * @link https://react.dev/reference/react/useSyncExternalStore */ -export function useLegacyStore( - store: T -): ReturnType { - const listener = useCallback( - (fn: () => void) => { - // Pass undefined to 2nd listen argument otherwise it explodes - return store.listen(fn, undefined) as () => void; - }, - [store] - ); - - return useSyncExternalStore(listener, store.getState); +export function useLegacyStore( + store: S +): ReturnType { + return useSelector(store.source); } diff --git a/static/app/types/reflux.d.ts b/static/app/types/reflux.d.ts deleted file mode 100644 index 88fc1e847b7f..000000000000 --- a/static/app/types/reflux.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type {Store, StoreDefinition} from 'reflux'; - -type RemoveIndex = { - [P in keyof T as string extends P ? never : P]: T[P]; -}; - -declare module 'reflux' { - function createStore( - storeDefinition: T - ): RemoveIndex; -} diff --git a/static/app/utils/routeAnalytics/setOrganizationCallback.tsx b/static/app/utils/routeAnalytics/setOrganizationCallback.tsx index c3bf5387c244..506be58dc119 100644 --- a/static/app/utils/routeAnalytics/setOrganizationCallback.tsx +++ b/static/app/utils/routeAnalytics/setOrganizationCallback.tsx @@ -2,7 +2,7 @@ import type {Organization} from 'sentry/types/organization'; /** * A module-level cell that bridges RouteAnalyticsContext (React) and - * organizationStore (Reflux). organizationStore cannot access React context + * organizationStore. organizationStore cannot access React context * directly, so useRouteAnalyticsHookSetup registers the setOrganization * state setter here and organizationStore calls it when org data loads. */ diff --git a/static/app/views/issueDetails/groupDetails.tsx b/static/app/views/issueDetails/groupDetails.tsx index 2e6012d67e66..9638aaf09940 100644 --- a/static/app/views/issueDetails/groupDetails.tsx +++ b/static/app/views/issueDetails/groupDetails.tsx @@ -207,7 +207,7 @@ function useSyncGroupStore(groupId: string, incomingEnvs: string[]) { // It's possible the overview page is still unloading the store useEffect(() => { - return GroupStore.listen(() => { + return GroupStore.subscribe(() => { const [storeGroup] = GroupStore.getState(); if ( defined(storeGroup) && @@ -226,7 +226,7 @@ function useSyncGroupStore(groupId: string, incomingEnvs: string[]) { prev => (prev ? {...prev, json: storeGroup as Group} : prev) ); } - }, undefined) as () => void; + }); }, [groupId, incomingEnvs, organization, queryClient]); } diff --git a/static/gsApp/actions/trialRequestedActions.tsx b/static/gsApp/actions/trialRequestedActions.tsx index 07bf16ff1b50..5593edce100f 100644 --- a/static/gsApp/actions/trialRequestedActions.tsx +++ b/static/gsApp/actions/trialRequestedActions.tsx @@ -1,6 +1,6 @@ -import Reflux from 'reflux'; +import {TrialRequestedStore} from 'getsentry/stores/trialRequestedStore'; -export const TrialRequestedActions = Reflux.createActions([ - 'requested', - 'clearNotification', -]); +export const TrialRequestedActions = { + requested: () => TrialRequestedStore.onRequested(), + clearNotification: () => TrialRequestedStore.onClearNotification(), +}; diff --git a/static/gsApp/components/trialStartedSidebarItem.tsx b/static/gsApp/components/trialStartedSidebarItem.tsx index 0e6d4df09100..d15299e2034a 100644 --- a/static/gsApp/components/trialStartedSidebarItem.tsx +++ b/static/gsApp/components/trialStartedSidebarItem.tsx @@ -111,9 +111,8 @@ function TrialStartedSidebarItem({subscription, organization, children}: Props) ); useEffect(() => { - const unsubscribe = TrialRequestedStore.listen( - () => setTrialRequested(TrialRequestedStore.getTrialRequstedState()), - undefined + const unsubscribe = TrialRequestedStore.subscribe(() => + setTrialRequested(TrialRequestedStore.getTrialRequstedState()) ); return () => { unsubscribe(); diff --git a/static/gsApp/components/withSubscription.tsx b/static/gsApp/components/withSubscription.tsx index d36a2f93877f..9868a970658f 100644 --- a/static/gsApp/components/withSubscription.tsx +++ b/static/gsApp/components/withSubscription.tsx @@ -77,10 +77,15 @@ export function withSubscription

( this.unsubscribe(); } - unsubscribe = SubscriptionStore.listen( - (subscription: Subscription) => this.onSubscriptionChange(subscription), - undefined - ); + private prevSubscriptions = SubscriptionStore.getState(); + unsubscribe = SubscriptionStore.subscribe(subscriptions => { + const orgSlug = this.getOrgSlug(); + const prevSubscriptions = this.prevSubscriptions; + this.prevSubscriptions = subscriptions; + if (orgSlug !== null && subscriptions[orgSlug] !== prevSubscriptions[orgSlug]) { + this.onSubscriptionChange(subscriptions[orgSlug]!); + } + }); private mounted = false; configureScopeWithSubscriptionData(subscription: Subscription) { diff --git a/static/gsApp/package.json b/static/gsApp/package.json index aa33add45401..3e6f3b2d284f 100644 --- a/static/gsApp/package.json +++ b/static/gsApp/package.json @@ -17,6 +17,7 @@ "@stripe/react-stripe-js": "^3.9.2", "@stripe/stripe-js": "^5.10.0", "@tanstack/react-query": "5.101.4", + "@tanstack/react-store": "0.11.2", "color": "5.0.2", "core-js": "3.45.0", "dompurify": "3.4.17", @@ -29,7 +30,6 @@ "query-string": "7.0.1", "react": "19.3.0", "react-router": "8.4.0", - "reflux": "0.4.1", "sentry": "workspace:@sentry-internal/app@*", "zod": "4.6.7" }, diff --git a/static/gsApp/stores/subscriptionStore.tsx b/static/gsApp/stores/subscriptionStore.tsx index 07306649fe2c..c872887edad6 100644 --- a/static/gsApp/stores/subscriptionStore.tsx +++ b/static/gsApp/stores/subscriptionStore.tsx @@ -1,7 +1,5 @@ -import {createStore} from 'reflux'; - import {Client} from 'sentry/api'; -import type {StrictStoreDefinition} from 'sentry/stores/types'; +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {Subscription} from 'getsentry/types'; @@ -16,105 +14,82 @@ type LoadOptions = { */ type Subscriptions = Record; -interface Internal { - api: Client; - callbacks: Record; - loadingData: Record; -} - -interface SubscriptionStoreDefintion - extends StrictStoreDefinition, Internal { - clearStartedTrial: (orgSlug: string) => void; - get: (orgSlug: string, callback: GetCallback) => void; - loadData: ( - orgSlug: string, - callback?: null | GetCallback, - options?: LoadOptions - ) => Promise; - set: (orgSlug: string, data: Partial) => void; -} +const store = createLegacyStore({}); -const subscriptionStoreConfig: SubscriptionStoreDefintion = { +export const SubscriptionStore = { + ...store, api: new Client(), - state: {}, - loadingData: {}, - callbacks: {}, + loadingData: {} as Record, + callbacks: {} as Record, init() { - this.api = new Client(); - this.state = {}; - this.loadingData = {}; - this.callbacks = {}; + SubscriptionStore.api = new Client(); + SubscriptionStore.loadingData = {}; + SubscriptionStore.callbacks = {}; + store.resetState({}); }, set(orgSlug: string, data: Partial) { const subscription = { - ...this.state[orgSlug]!, + ...store.getState()[orgSlug]!, ...data, setAt: Date.now(), // Refetch usage data if Subscription is updated }; - this.state = {...this.state, [orgSlug]: subscription}; - this.trigger(subscription); + store.setState(prev => ({...prev, [orgSlug]: subscription})); - if (this.callbacks[orgSlug]) { - this.callbacks[orgSlug].forEach(cb => cb(subscription)); - delete this.callbacks[orgSlug]; + const callbacks = SubscriptionStore.callbacks[orgSlug]; + if (callbacks) { + callbacks.forEach(cb => cb(subscription)); + delete SubscriptionStore.callbacks[orgSlug]; } }, get(orgSlug: string, callback: GetCallback) { - if (this.state[orgSlug]) { - callback(this.state[orgSlug]); + const subscription = store.getState()[orgSlug]; + if (subscription) { + callback(subscription); return; } - this.loadData(orgSlug, callback); - }, - - getState() { - return this.state; + SubscriptionStore.loadData(orgSlug, callback); }, clearStartedTrial(orgSlug: string) { - if (!this.state[orgSlug]) { + const current = store.getState()[orgSlug]; + if (!current) { return; } - // Do not use this.set() as there is no new data and we do not need to + // Do not use set() as there is no new data and we do not need to // update subscription.setAt // Remove isTrialStarted from subscription - const {isTrialStarted: _, ...subscription} = this.state[orgSlug]; - this.state = {...this.state, [orgSlug]: subscription}; - this.trigger(this.state[orgSlug]); + const {isTrialStarted: _, ...subscription} = current; + store.setState(prev => ({...prev, [orgSlug]: subscription})); }, async loadData( orgSlug: string, callback?: null | GetCallback, - {markStartedTrial} = {} + {markStartedTrial}: LoadOptions = {} ) { - if (this.callbacks[orgSlug] === undefined) { - this.callbacks[orgSlug] = []; - } + const callbacks = (SubscriptionStore.callbacks[orgSlug] ??= []); if (typeof callback === 'function') { - this.callbacks[orgSlug].push(callback); + callbacks.push(callback); } - if (this.loadingData[orgSlug]) { + if (SubscriptionStore.loadingData[orgSlug]) { return; } - this.loadingData[orgSlug] = true; + SubscriptionStore.loadingData[orgSlug] = true; - const data = await this.api.requestPromise(`/customers/${orgSlug}/`); + const data = await SubscriptionStore.api.requestPromise(`/customers/${orgSlug}/`); if (markStartedTrial) { data.isTrialStarted = true; } - this.set(orgSlug, data); - delete this.loadingData[orgSlug]; + SubscriptionStore.set(orgSlug, data); + delete SubscriptionStore.loadingData[orgSlug]; }, }; - -export const SubscriptionStore = createStore(subscriptionStoreConfig); diff --git a/static/gsApp/stores/trialRequestedStore.tsx b/static/gsApp/stores/trialRequestedStore.tsx index f4239b0f54ba..9250db1e9bed 100644 --- a/static/gsApp/stores/trialRequestedStore.tsx +++ b/static/gsApp/stores/trialRequestedStore.tsx @@ -1,40 +1,23 @@ -import Reflux from 'reflux'; - -import {TrialRequestedActions} from 'getsentry/actions/trialRequestedActions'; +import {createLegacyStore} from 'sentry/stores/createLegacyStore'; type State = { requested: boolean; }; -type TrialRequestedStoreInterface = { - getTrialRequstedState: () => State['requested']; - state: State; -}; - -const storeConfig: Reflux.StoreDefinition & TrialRequestedStoreInterface = { - state: { - requested: false, - }, +const store = createLegacyStore({requested: false}); - init() { - this.listenTo(TrialRequestedActions.requested, this.onRequested); - this.listenTo(TrialRequestedActions.clearNotification, this.onClearNotification); - }, +export const TrialRequestedStore = { + ...store, onRequested() { - this.state = {...this.state, requested: true}; - this.trigger(this.state); + store.setState(prev => ({...prev, requested: true})); }, onClearNotification() { - this.state = {...this.state, requested: false}; - this.trigger(this.state); + store.setState(prev => ({...prev, requested: false})); }, getTrialRequstedState() { - return this.state.requested; + return store.getState().requested; }, }; - -export const TrialRequestedStore = Reflux.createStore(storeConfig) as Reflux.Store & - TrialRequestedStoreInterface; From 49df96069d7411d7d88595d566fbdcd48d312ddd Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Tue, 27 Oct 2026 15:33:00 -0700 Subject: [PATCH 2/7] chore(stores): Drop leftover reflux types and jest config @types/reflux, the jest unmock entry and a couple of comments still pointed at reflux. Co-Authored-By: Claude Opus 5.5 --- pnpm-lock.yaml | 11 ----------- static/app/package.json | 1 - static/app/stores/createLegacyStore.tsx | 2 +- static/app/views/seerExplorer/xray/xrayModeStore.ts | 2 +- static/gsApp/package.json | 3 +-- tests/js/createJestConfig.ts | 5 +---- 6 files changed, 4 insertions(+), 20 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c12a26f51c5a..c584900df613 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -863,9 +863,6 @@ importers: '@types/react-select': specifier: 4.0.18 version: 4.0.18 - '@types/reflux': - specifier: 0.4.1 - version: 0.4.1 expect-type: specifier: 1.4.0 version: 1.4.0 @@ -1054,9 +1051,6 @@ importers: '@types/react': specifier: 19.3.0 version: 19.3.0 - '@types/reflux': - specifier: 0.4.1 - version: 0.4.1 static/oxlint/eslintPluginScraps: dependencies: @@ -4765,9 +4759,6 @@ packages: '@types/readable-stream@4.0.18': resolution: {integrity: sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==} - '@types/reflux@0.4.1': - resolution: {integrity: sha512-XOavgwrJxBiSQufelsZ+ZAi6Xvrh/M/IlfKNrqnxHytK8WKkWF8G4QhZwwhLXk7NsgaUf3ZqcFFN9YhzBqmuNA==} - '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -12372,8 +12363,6 @@ snapshots: '@types/node': 26.6.4 safe-buffer: 5.1.2 - '@types/reflux@0.4.1': {} - '@types/stack-utils@2.0.3': {} '@types/tough-cookie@4.0.5': {} diff --git a/static/app/package.json b/static/app/package.json index 032d929afba5..a2f80ac5b282 100644 --- a/static/app/package.json +++ b/static/app/package.json @@ -144,7 +144,6 @@ "@types/react-grid-layout": "^1.3.6", "@types/react-lazyload": "3.2.3", "@types/react-select": "4.0.18", - "@types/reflux": "0.4.1", "expect-type": "1.4.0", "jest-fetch-mock": "4.2.0", "mockdate": "3.0.5", diff --git a/static/app/stores/createLegacyStore.tsx b/static/app/stores/createLegacyStore.tsx index 278e8e5b3a14..669e10732da5 100644 --- a/static/app/stores/createLegacyStore.tsx +++ b/static/app/stores/createLegacyStore.tsx @@ -4,7 +4,7 @@ import {Store} from '@tanstack/react-store'; * Wraps a TanStack Store with the `getState`/`subscribe` shape our stores * expose, and lets callers replace the state without notifying subscribers. * - * Reflux stores reset their state silently, and many callers (mostly tests + * Our stores have always reset their state silently, and many callers (mostly tests * resetting stores in `afterEach` while components are still mounted) rely on * that. Use `resetState` for those writes and `setState` for everything else. */ diff --git a/static/app/views/seerExplorer/xray/xrayModeStore.ts b/static/app/views/seerExplorer/xray/xrayModeStore.ts index 6bd63f7eb4b2..c97538929660 100644 --- a/static/app/views/seerExplorer/xray/xrayModeStore.ts +++ b/static/app/views/seerExplorer/xray/xrayModeStore.ts @@ -5,7 +5,7 @@ import {localStorageWrapper} from 'sentry/utils/localStorage'; /** * Seer XRay Mode — on/off state. * - * A minimal module-level store (not Reflux) so the cmd+K toggle action and + * A minimal module-level store so the cmd+K toggle action and * the overlay, which live in unrelated parts of the tree, can share one flag * without a dedicated context provider. Persisted to localStorage so the * mode survives a page reload. diff --git a/static/gsApp/package.json b/static/gsApp/package.json index 3e6f3b2d284f..c1376f90923b 100644 --- a/static/gsApp/package.json +++ b/static/gsApp/package.json @@ -37,7 +37,6 @@ "@types/history": "^3.2.5", "@types/js-cookie": "3.0.6", "@types/lodash": "^4.17.24", - "@types/react": "19.3.0", - "@types/reflux": "0.4.1" + "@types/react": "19.3.0" } } diff --git a/tests/js/createJestConfig.ts b/tests/js/createJestConfig.ts index c2de5c3b9560..5cbe6480b865 100644 --- a/tests/js/createJestConfig.ts +++ b/tests/js/createJestConfig.ts @@ -318,10 +318,7 @@ const config: Config.InitialOptions = { // jest.mock() is decided by crawl order. Keep the module map to this checkout. modulePathIgnorePatterns: ['/\\.claude/'], - unmockedModulePathPatterns: [ - '/node_modules/react', - '/node_modules/reflux', - ], + unmockedModulePathPatterns: ['/node_modules/react'], transform: { '^.+\\.[mc]?[jt]sx?$': [fileURLToPath(import.meta.resolve('@swc/jest')), swcConfig], '^.+\\.pegjs?$': '/tests/js/jest-pegjs-transform.js', From 298ee4b72a39769f13172962d6cfa31bb4c5b0a1 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Tue, 27 Oct 2026 15:52:25 -0700 Subject: [PATCH 3/7] ref(stores): Use tanstack stores directly 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 --- .github/codeowners-coverage-baseline.txt | 1 - .github/dependabot.yml | 2 - knip.config.ts | 2 - static/app/components/acl/feature.tsx | 8 +-- .../app/components/assistant/guideAnchor.tsx | 7 ++- .../ui/commandPaletteGlobalActions.tsx | 19 ++++--- static/app/components/contextPickerModal.tsx | 9 ++-- static/app/components/core/modal/index.tsx | 11 ++-- .../drawer/useDroppedDataDrawer.spec.tsx | 2 +- .../droppedDataFilterNote.spec.tsx | 8 +-- .../droppedDataLLMContext.spec.tsx | 5 +- .../droppedData/useDroppedData.spec.tsx | 5 +- .../featureFlagOnboardingLayout.tsx | 5 +- .../featureFlagOnboardingSidebar.tsx | 4 +- .../featureFeedback/feedbackModal.tsx | 4 +- .../feedbackOnboardingLayout.tsx | 5 +- .../feedback/feedbackOnboarding/sidebar.tsx | 4 +- .../feedback/list/mailboxPicker.tsx | 5 +- .../useFeedbackSDKIntegration.tsx | 8 +-- .../app/components/frontendVersionContext.tsx | 4 +- static/app/components/idBadge/teamBadge.tsx | 5 +- .../modals/dataWidgetViewerModal.spec.tsx | 2 +- .../gettingStartedDoc/onboardingLayout.tsx | 5 +- .../utils/useCurrentProjectState.ts | 5 +- .../scm/useScmProjectCreation.spec.tsx | 4 +- .../scm/useScmProjectDetails.spec.tsx | 2 +- .../components/pageFilters/container.spec.tsx | 2 +- .../environmentPageFilter.spec.tsx | 3 +- .../project/projectPageFilter.spec.tsx | 3 +- .../app/components/pageFilters/store.spec.tsx | 6 +-- static/app/components/pageFilters/store.tsx | 31 ++++++----- .../components/pageFilters/usePageFilters.tsx | 5 +- .../performanceOnboarding/sidebar.tsx | 9 ++-- static/app/components/platformPicker.tsx | 4 +- .../profiling/profilingOnboardingSidebar.tsx | 7 +-- .../missingProjectMembership.spec.tsx | 5 +- .../projects/missingProjectMembership.tsx | 12 ++--- .../replayOnboardingLayout.tsx | 5 +- .../components/replaysOnboarding/sidebar.tsx | 4 +- .../projectAddRepoModal.spec.tsx | 2 +- .../projectTable/seerProjectTable.spec.tsx | 2 +- .../app/components/themeAndStyleProvider.tsx | 8 +-- static/app/components/updatedEmptyState.tsx | 5 +- .../components/debugNotificationsHeader.tsx | 5 +- static/app/stores/IssueListCacheStore.tsx | 24 +++++---- static/app/stores/configStore.tsx | 16 +++--- static/app/stores/createLegacyStore.tsx | 54 ------------------- static/app/stores/demoWalkthroughStore.tsx | 15 ++++-- static/app/stores/groupStore.spec.tsx | 4 +- static/app/stores/groupStore.tsx | 15 ++++-- static/app/stores/guideStore.tsx | 35 ++++++------ static/app/stores/modalStore.tsx | 32 +++++------ static/app/stores/onboardingDrawerStore.tsx | 12 +++-- static/app/stores/organizationStore.tsx | 15 ++++-- static/app/stores/organizationsStore.tsx | 23 ++++---- static/app/stores/projectsStore.tsx | 47 ++++++++-------- static/app/stores/tagStore.spec.tsx | 2 +- static/app/stores/tagStore.tsx | 15 ++++-- static/app/stores/teamStore.tsx | 37 ++++++------- static/app/stores/useLegacyStore.spec.tsx | 22 -------- static/app/stores/useLegacyStore.tsx | 18 ------- static/app/stories/theme.tsx | 5 +- .../useAssignedSearchValues.tsx | 4 +- .../flamegraph/flamegraphThemeProvider.tsx | 4 +- .../useFetchEventsTimeSeries.spec.tsx | 2 +- static/app/utils/useProjects.tsx | 27 +++++----- static/app/utils/useServiceIncidents.tsx | 4 +- static/app/utils/useTags.tsx | 5 +- static/app/utils/useTeams.tsx | 6 +-- static/app/utils/useTeamsById.tsx | 17 +++--- static/app/utils/useUser.tsx | 7 +-- static/app/utils/useUserTeams.tsx | 20 +++---- static/app/views/app/index.tsx | 37 ++++++++----- static/app/views/app/root.tsx | 7 ++- .../hooks/useSingleOrganizationLogin.tsx | 7 ++- .../app/views/authV2/authRegister/index.tsx | 5 +- .../dashboards/adjustedFiltersAlert.spec.tsx | 2 +- .../prebuiltConfigs/ai/mcpOverview.spec.tsx | 5 +- .../components/newWidgetBuilder.spec.tsx | 3 +- .../detailsWidgetVisualization.spec.tsx | 2 +- .../components/details/cron/index.spec.tsx | 4 +- static/app/views/discover/results.spec.tsx | 5 +- .../discover/results/resultsChart.spec.tsx | 5 +- .../views/discover/table/tableView.spec.tsx | 5 +- .../components/agentsTable.spec.tsx | 2 +- .../useShowConversationOnboarding.spec.tsx | 4 +- .../explore/conversations/onboarding.spec.tsx | 2 +- .../explore/conversations/onboarding.tsx | 5 +- .../explore/conversations/overview.spec.tsx | 2 +- .../hooks/useAttributeBreakdowns.spec.tsx | 5 +- .../hooks/useExploreAggregatesTable.spec.tsx | 5 +- .../hooks/useExploreSpansTable.spec.tsx | 5 +- .../hooks/useExploreTimeseries.spec.tsx | 5 +- .../views/explore/hooks/useSaveQuery.spec.tsx | 2 +- .../app/views/explore/logs/logsOnboarding.tsx | 5 +- .../views/explore/logs/useLogsQuery.spec.tsx | 4 +- .../useLogsSearchQueryBuilderProps.spec.tsx | 2 +- .../logs/useLogsTimeseriesRequest.spec.tsx | 5 +- .../explore/logs/useSaveAsItems.spec.tsx | 7 +-- .../explore/logs/useValidateLogsTab.spec.tsx | 2 +- ...metricsAggregateExportModalButton.spec.tsx | 5 +- .../metricsSamplesExportModalButton.spec.tsx | 5 +- .../hooks/useMetricAggregatesTable.spec.tsx | 5 +- .../hooks/useMetricSamplesTable.spec.tsx | 5 +- .../hooks/useMetricTimeseries.spec.tsx | 5 +- .../hooks/useSaveMetricsMultiQuery.spec.tsx | 2 +- .../hooks/useValidateMetricsTab.spec.tsx | 2 +- .../explore/metrics/metricsOnboarding.tsx | 5 +- .../metrics/useSaveAsMetricItems.spec.tsx | 5 +- .../hooks/useMultiQueryTimeseries.spec.tsx | 5 +- .../views/explore/profiling/onboarding.tsx | 5 +- .../explore/releases/list/index.spec.tsx | 2 +- .../header/replayDetailsEntityHeader.spec.tsx | 5 +- .../views/explore/spans/charts/index.spec.tsx | 5 +- static/app/views/explore/spans/content.tsx | 7 ++- .../spans/hooks/useValidateSpansTab.spec.tsx | 2 +- .../explore/tables/aggregatesTable.spec.tsx | 5 +- .../explore/tables/fieldRenderer.spec.tsx | 2 +- .../components/fullSpanDescription.spec.tsx | 5 +- .../components/modulePageProviders.spec.tsx | 2 +- .../components/spanDescription.spec.tsx | 5 +- .../queries/useSortedTimeSeries.spec.tsx | 5 +- .../utils/useDefaultToAllProjects.spec.tsx | 4 +- .../components/noDataMessage.spec.tsx | 2 +- .../queries/useCrossPlatformProject.spec.tsx | 7 +-- .../insights/pages/agents/onboarding.spec.tsx | 5 +- .../insights/pages/agents/onboarding.tsx | 5 +- .../views/insights/pages/mcp/onboarding.tsx | 5 +- .../issueDetails/actions/shareModal.spec.tsx | 2 +- .../views/issueDetails/actions/shareModal.tsx | 4 +- .../views/issueDetails/groupDetails.spec.tsx | 6 +-- .../app/views/issueDetails/groupDetails.tsx | 4 +- static/app/views/issueDetails/utils.tsx | 4 +- static/app/views/issueList/overview.tsx | 4 +- .../issueList/pages/inbox/index.spec.tsx | 2 +- .../app/views/navigation/primary/helpMenu.tsx | 5 +- .../views/navigation/primary/onboarding.tsx | 4 +- .../primary/organizationDropdown.spec.tsx | 5 +- .../primary/organizationDropdown.tsx | 16 ++++-- .../app/views/onboarding/onboarding.spec.tsx | 2 +- static/app/views/organizationContainer.tsx | 6 ++- static/app/views/organizationContext.tsx | 21 +++++--- static/app/views/organizationCreate/index.tsx | 9 ++-- .../views/organizationStats/index.spec.tsx | 5 +- .../eap/segmentSpansTable.spec.tsx | 5 +- .../app/views/performance/onboarding.spec.tsx | 5 +- static/app/views/performance/onboarding.tsx | 5 +- .../views/performance/traceDetails/trace.tsx | 4 +- .../eapSampledEventsTab.spec.tsx | 2 +- .../eapSampledEventsTable.spec.tsx | 2 +- .../projectDetail/projectDetail.spec.tsx | 5 +- .../views/projectsDashboard/index.spec.tsx | 2 +- static/app/views/routeError.tsx | 4 +- .../account/accountNotificationFineTuning.tsx | 11 ++-- ...ccountNotificationFineTuningController.tsx | 5 +- .../accountSecurityWrapper.tsx | 4 +- .../notificationSettingsByType.spec.tsx | 2 +- .../notificationSettingsByType.tsx | 7 ++- .../notificationSettingsController.tsx | 5 +- .../views/settings/earlyFeatures/index.tsx | 4 +- .../integrationCodeMappings.spec.tsx | 2 +- .../inviteRequestRow.spec.tsx | 6 ++- .../organizationTeams.spec.tsx | 6 ++- .../organizationTeams/teamDetails.spec.tsx | 2 +- .../settings/projectGeneralSettings/index.tsx | 4 +- .../utils/useOrganizationsWithRegion.tsx | 4 +- static/gsApp/components/stripeWrapper.tsx | 4 +- .../components/trialStartedSidebarItem.tsx | 2 +- static/gsApp/components/withSubscription.tsx | 4 +- static/gsApp/hooks/useSubscription.tsx | 5 +- .../overrides/useBillingNavigationConfig.tsx | 5 +- static/gsApp/stores/subscriptionStore.tsx | 19 ++++--- static/gsApp/stores/trialRequestedStore.tsx | 12 +++-- .../views/amCheckout/components/cartDiff.tsx | 6 +-- .../components/index.spec.tsx | 5 +- 175 files changed, 612 insertions(+), 662 deletions(-) delete mode 100644 static/app/stores/createLegacyStore.tsx delete mode 100644 static/app/stores/useLegacyStore.spec.tsx delete mode 100644 static/app/stores/useLegacyStore.tsx diff --git a/.github/codeowners-coverage-baseline.txt b/.github/codeowners-coverage-baseline.txt index f163d727e092..84ec06acf222 100644 --- a/.github/codeowners-coverage-baseline.txt +++ b/.github/codeowners-coverage-baseline.txt @@ -1249,7 +1249,6 @@ static/app/types/onboarding.tsx static/app/types/organization.tsx static/app/types/profiling/core.tsx static/app/types/reducerAction.tsx -static/app/types/reflux.d.ts static/app/types/relay.tsx static/app/types/release.tsx static/app/types/sampling.tsx diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c0728ee15cd5..f003312ca62e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -75,11 +75,9 @@ updates: # We ignore everything that hasn't yet been upgrade, this way we will # only get the _freshest_ of new packages to consider upgrading - dependency-name: '@types/react-select' - - dependency-name: '@types/reflux' - dependency-name: 'gettext-parser' - dependency-name: 'react-lazyload' - dependency-name: 'react-select' - - dependency-name: 'reflux' - dependency-name: 'sprintf-js' - package-ecosystem: 'docker' directory: 'self-hosted/' diff --git a/knip.config.ts b/knip.config.ts index 45731961c170..089531b5c809 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -79,8 +79,6 @@ const config: KnipConfig = { 'tslib', // subdependency of many packages, declare the latest version 'odiff-bin', // raw binary consumed by Python backend, not a JS import 'zrender', // used in echarts - // Referenced by its node_modules path in tests/js/createJestConfig.ts. - 'reflux', ], // Knip's Less compiler expects the extension in `project`; styles are handled by Rspack, // so do not report them as unused files. diff --git a/static/app/components/acl/feature.tsx b/static/app/components/acl/feature.tsx index 364454a30f27..919240f4c77c 100644 --- a/static/app/components/acl/feature.tsx +++ b/static/app/components/acl/feature.tsx @@ -1,8 +1,8 @@ import {useMemo} from 'react'; +import {useSelector} from '@tanstack/react-store'; import {getOverride} from 'sentry/overrideRegistry'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Organization} from 'sentry/types/organization'; import type {FeatureDisabledOverrides} from 'sentry/types/overrides'; import type {Project} from 'sentry/types/project'; @@ -138,7 +138,7 @@ function Feature({ renderDisabled = false, requireAll = true, }: Props) { - const config = useLegacyStore(ConfigStore); + const configFeatures = useSelector(ConfigStore.store, state => state.features); const features = useMemo( () => (Array.isArray(featuresProp) ? featuresProp : [featuresProp]), @@ -147,11 +147,11 @@ function Feature({ const allFeatures = useMemo( () => ({ - configFeatures: config.features ? Array.from(config.features) : [], + configFeatures: configFeatures ? Array.from(configFeatures) : [], organization: organization?.features ?? [], project: project?.features ?? [], }), - [config.features, organization?.features, project?.features] + [configFeatures, organization?.features, project?.features] ); const hasFeatureEnabled = useMemo(() => { diff --git a/static/app/components/assistant/guideAnchor.tsx b/static/app/components/assistant/guideAnchor.tsx index 9c056b1bde55..04b2f6d912c0 100644 --- a/static/app/components/assistant/guideAnchor.tsx +++ b/static/app/components/assistant/guideAnchor.tsx @@ -1,5 +1,6 @@ import {useCallback, useEffect, useRef} from 'react'; import * as Sentry from '@sentry/react'; +import {useSelector} from '@tanstack/react-store'; import {Container, Grid} from '@sentry/scraps/layout'; @@ -15,7 +16,6 @@ import type {Hovercard} from 'sentry/components/hovercard'; import {TourAction, TourGuide} from 'sentry/components/tours/components'; import {t} from 'sentry/locale'; import {GuideStore} from 'sentry/stores/guideStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; interface Props { target: string; @@ -64,7 +64,10 @@ function BaseGuideAnchor({ onFinish, onStepComplete, }: Props) { - const {currentGuide, currentStep: step, orgId, forceHide} = useLegacyStore(GuideStore); + const currentGuide = useSelector(GuideStore.store, state => state.currentGuide); + const step = useSelector(GuideStore.store, state => state.currentStep); + const orgId = useSelector(GuideStore.store, state => state.orgId); + const forceHide = useSelector(GuideStore.store, state => state.forceHide); useEffect(() => { registerAnchor(target); diff --git a/static/app/components/commandPalette/ui/commandPaletteGlobalActions.tsx b/static/app/components/commandPalette/ui/commandPaletteGlobalActions.tsx index e044016541f6..9ef12e25d7d5 100644 --- a/static/app/components/commandPalette/ui/commandPaletteGlobalActions.tsx +++ b/static/app/components/commandPalette/ui/commandPaletteGlobalActions.tsx @@ -28,6 +28,7 @@ import {IconTerminal} from '@sentry/icons/terminal'; import {IconUser} from '@sentry/icons/user'; import * as Sentry from '@sentry/react'; import {skipToken, useMutation, useQuery} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import DOMPurify from 'dompurify'; import { @@ -57,7 +58,6 @@ import {DEPLOY_PREVIEW_CONFIG, NODE_ENV} from 'sentry/constants'; import {t, toggleLocaleDebug} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; import {OrganizationsStore} from 'sentry/stores/organizationsStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {EventIdResponse} from 'sentry/types/event'; import type {ShortIdResponse} from 'sentry/types/group'; import type {Member, Team} from 'sentry/types/organization'; @@ -309,8 +309,15 @@ export function GlobalCommandPaletteActions() { const user = useUser(); const isSentryEmployee = useIsSentryEmployee(); const {projects} = useProjects(); - const {organizations} = useLegacyStore(OrganizationsStore); - const sentryConfig = useLegacyStore(ConfigStore); + const organizations = useSelector( + OrganizationsStore.store, + state => state.organizations + ); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const singleOrganization = useSelector( + ConfigStore.store, + state => state.singleOrganization + ); const params = useParams(); const location = useLocation(); const {mutateAsync: mutateUserOptions} = useMutateUserOptions(); @@ -362,7 +369,7 @@ export function GlobalCommandPaletteActions() { const context: Omit = { access: new Set(organization.access), features: new Set(organization.features), - isSelfHosted: sentryConfig.isSelfHosted, + isSelfHosted, organization, }; return getUserOrgNavigationConfiguration() @@ -372,7 +379,7 @@ export function GlobalCommandPaletteActions() { ) ) .sort((a, b) => a.title.localeCompare(b.title)); - }, [organization, sentryConfig.isSelfHosted]); + }, [organization, isSelfHosted]); const prefix = `/organizations/${organization.slug}`; const hasInsightsRollout = organization.features.includes( @@ -638,7 +645,7 @@ export function GlobalCommandPaletteActions() { to={makeProjectsPathname({path: '/', organization})} /> - {!sentryConfig.singleOrganization && !isDemoModeActive() && ( + {!singleOrganization && !isDemoModeActive() && ( }} keywords={[t('organization'), t('change'), t('change organization')]} diff --git a/static/app/components/contextPickerModal.tsx b/static/app/components/contextPickerModal.tsx index aedec286909e..2c39d78599af 100644 --- a/static/app/components/contextPickerModal.tsx +++ b/static/app/components/contextPickerModal.tsx @@ -9,6 +9,7 @@ import { type SetStateAction, } from 'react'; import {skipToken, useQuery} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import type {Query} from 'history'; import {ProjectAvatar, TeamAvatar} from '@sentry/scraps/avatar'; @@ -25,7 +26,6 @@ import {t, tct} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; import {OrganizationsStore} from 'sentry/stores/organizationsStore'; import {OrganizationStore} from 'sentry/stores/organizationStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Integration} from 'sentry/types/integrations'; import type {OrganizationSummary, Team} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; @@ -612,8 +612,11 @@ export function ContextPickerModalContainer({ projectSlugs, ...sharedProps }: ContainerProps) { - const {organizations} = useLegacyStore(OrganizationsStore); - const {organization} = useLegacyStore(OrganizationStore); + const organizations = useSelector( + OrganizationsStore.store, + state => state.organizations + ); + const organization = useSelector(OrganizationStore.store, state => state.organization); const [selectedOrgSlug, setSelectedOrgSlug] = useState(organization?.slug); if (configQueryKey) { diff --git a/static/app/components/core/modal/index.tsx b/static/app/components/core/modal/index.tsx index c64238e3dce8..cae4f66830a2 100644 --- a/static/app/components/core/modal/index.tsx +++ b/static/app/components/core/modal/index.tsx @@ -1,6 +1,7 @@ import {useCallback, useEffect, useRef, type ComponentProps} from 'react'; import {createPortal} from 'react-dom'; import {css, type Interpolation, type Theme, useTheme} from '@emotion/react'; +import {useSelector} from '@tanstack/react-store'; import type {FocusTrap} from 'focus-trap'; import {createFocusTrap} from 'focus-trap'; import {AnimatePresence, motion} from 'framer-motion'; @@ -12,7 +13,6 @@ import {useScrollLock} from '@sentry/scraps/useScrollLock'; import {ROOT_ELEMENT} from 'sentry/constants'; import {ModalStore} from 'sentry/stores/modalStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {getModalPortal} from 'sentry/utils/getModalPortal'; import {useEffectAfterFirstRender} from 'sentry/utils/useEffectAfterFirstRender'; import {useLocation} from 'sentry/utils/useLocation'; @@ -99,7 +99,7 @@ export type ModalTypes = { }; function useModalStore() { - const store = useLegacyStore(ModalStore); + const store = useSelector(ModalStore.store); return { ...store, visible: typeof store.renderer === 'function', @@ -333,7 +333,8 @@ interface UseModalReturn { } export function useModal(): UseModalReturn { - const store = useLegacyStore(ModalStore); + const modalRenderer = useSelector(ModalStore.store, state => state.renderer); + const focusTrap = useSelector(ModalStore.store, state => state.focusTrap); const openModal = useCallback( ( @@ -349,14 +350,14 @@ export function useModal(): UseModalReturn { ModalStore.closeModal(); }, []); - const isOpen = typeof store.renderer === 'function'; + const isOpen = typeof modalRenderer === 'function'; return { openModal, closeModal: closeModalFn, isOpen, visible: isOpen, - focusTrap: store.focusTrap, + focusTrap, }; } diff --git a/static/app/components/droppedData/drawer/useDroppedDataDrawer.spec.tsx b/static/app/components/droppedData/drawer/useDroppedDataDrawer.spec.tsx index a4ec7546d7f1..5482a3a80efc 100644 --- a/static/app/components/droppedData/drawer/useDroppedDataDrawer.spec.tsx +++ b/static/app/components/droppedData/drawer/useDroppedDataDrawer.spec.tsx @@ -79,6 +79,7 @@ function DroppedDataTrigger({enabled, interval}: {enabled?: boolean; interval?: describe('useDroppedDataDrawer', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState( PageFiltersFixture({ datetime: {period: '14d', start: null, end: null, utc: false}, @@ -87,7 +88,6 @@ describe('useDroppedDataDrawer', () => { }); afterEach(() => { - PageFiltersStore.reset(); localStorage.clear(); }); diff --git a/static/app/components/droppedData/droppedDataFilterNote.spec.tsx b/static/app/components/droppedData/droppedDataFilterNote.spec.tsx index 8575aefae5df..3b5f7206e536 100644 --- a/static/app/components/droppedData/droppedDataFilterNote.spec.tsx +++ b/static/app/components/droppedData/droppedDataFilterNote.spec.tsx @@ -7,14 +7,14 @@ import {PageFiltersStore} from 'sentry/components/pageFilters/store'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; describe('DroppedDataFilterNote', () => { + beforeEach(() => { + PageFiltersStore.reset(); + }); + function setEnvironments(environments: string[]) { PageFiltersStore.onInitializeUrlState(PageFiltersFixture({environments})); } - afterEach(() => { - PageFiltersStore.reset(); - }); - it('renders nothing without a search or environment', () => { setEnvironments([]); const {container} = render( diff --git a/static/app/components/droppedData/droppedDataLLMContext.spec.tsx b/static/app/components/droppedData/droppedDataLLMContext.spec.tsx index 7a6c0cc1b280..b85e0c1e14bc 100644 --- a/static/app/components/droppedData/droppedDataLLMContext.spec.tsx +++ b/static/app/components/droppedData/droppedDataLLMContext.spec.tsx @@ -30,11 +30,8 @@ function findDroppedDataNode(snapshot: LLMContextSnapshot) { describe('DroppedDataLLMContext', () => { beforeEach(() => { - PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); - }); - - afterEach(() => { PageFiltersStore.reset(); + PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); }); it('registers nothing without the feature flag', () => { diff --git a/static/app/components/droppedData/useDroppedData.spec.tsx b/static/app/components/droppedData/useDroppedData.spec.tsx index 4419fa9d992f..0dc00b38f956 100644 --- a/static/app/components/droppedData/useDroppedData.spec.tsx +++ b/static/app/components/droppedData/useDroppedData.spec.tsx @@ -52,11 +52,8 @@ function ChartWithLateConsumer() { describe('useDroppedData', () => { beforeEach(() => { - PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); - }); - - afterEach(() => { PageFiltersStore.reset(); + PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); }); it('requests dropped events and returns them', async () => { diff --git a/static/app/components/events/featureFlags/onboarding/featureFlagOnboardingLayout.tsx b/static/app/components/events/featureFlags/onboarding/featureFlagOnboardingLayout.tsx index 3ea80c7be941..3a29942d30d5 100644 --- a/static/app/components/events/featureFlags/onboarding/featureFlagOnboardingLayout.tsx +++ b/static/app/components/events/featureFlags/onboarding/featureFlagOnboardingLayout.tsx @@ -1,5 +1,6 @@ import {useMemo} from 'react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import {LinkButton} from '@sentry/scraps/button'; @@ -14,7 +15,6 @@ import {useSourcePackageRegistries} from 'sentry/components/onboarding/gettingSt import {useUrlPlatformOptions} from 'sentry/components/onboarding/platformOptionsControl'; import {t} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {useApi} from 'sentry/utils/useApi'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -36,7 +36,8 @@ export function FeatureFlagOnboardingLayout({ const {isPending: isLoadingRegistry, data: registryData} = useSourcePackageRegistries(organization); const selectedOptions = useUrlPlatformOptions(docsConfig.platformOptions); - const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const urlPrefix = useSelector(ConfigStore.store, state => state.urlPrefix); const {steps} = useMemo(() => { const doc = docsConfig[configType] ?? docsConfig.onboarding; diff --git a/static/app/components/events/featureFlags/onboarding/featureFlagOnboardingSidebar.tsx b/static/app/components/events/featureFlags/onboarding/featureFlagOnboardingSidebar.tsx index 828e8f38882b..c820c6f63809 100644 --- a/static/app/components/events/featureFlags/onboarding/featureFlagOnboardingSidebar.tsx +++ b/static/app/components/events/featureFlags/onboarding/featureFlagOnboardingSidebar.tsx @@ -1,6 +1,7 @@ import type {ReactNode} from 'react'; import {Fragment, useEffect, useMemo, useRef, useState} from 'react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import {parseAsString, useQueryState} from 'nuqs'; import HighlightTopRightPattern from 'sentry-images/pattern/highlight-top-right.svg'; @@ -29,7 +30,6 @@ import { OnboardingDrawerKey, OnboardingDrawerStore, } from 'sentry/stores/onboardingDrawerStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -37,7 +37,7 @@ import {TextBlock} from 'sentry/views/settings/components/text/textBlock'; export function useFeatureFlagOnboardingDrawer() { const organization = useOrganization(); - const currentPanel = useLegacyStore(OnboardingDrawerStore); + const currentPanel = useSelector(OnboardingDrawerStore.store); const isActive = currentPanel === OnboardingDrawerKey.FEATURE_FLAG_ONBOARDING; const hasProjectAccess = organization.access.includes('project:read'); const initialPathname = useRef(null); diff --git a/static/app/components/featureFeedback/feedbackModal.tsx b/static/app/components/featureFeedback/feedbackModal.tsx index a6402813b48a..0d355ee9d802 100644 --- a/static/app/components/featureFeedback/feedbackModal.tsx +++ b/static/app/components/featureFeedback/feedbackModal.tsx @@ -8,6 +8,7 @@ import { getDefaultIntegrations, makeFetchTransport, } from '@sentry/react'; +import {useSelector} from '@tanstack/react-store'; import cloneDeep from 'lodash/cloneDeep'; import {Alert} from '@sentry/scraps/alert'; @@ -24,7 +25,6 @@ import type {Data} from 'sentry/components/forms/types'; import {t, tct} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; import {OrganizationStore} from 'sentry/stores/organizationStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {defined} from 'sentry/utils/defined'; import {useLocation} from 'sentry/utils/useLocation'; import {useProjects} from 'sentry/utils/useProjects'; @@ -205,7 +205,7 @@ export function FeedbackModal({ closeModal, ...props }: FeedbackModalProps & ModalRenderProps) { - const {organization} = useLegacyStore(OrganizationStore); + const organization = useSelector(OrganizationStore.store, state => state.organization); const {projects, initiallyLoaded: projectsLoaded} = useProjects(); const location = useLocation(); diff --git a/static/app/components/feedback/feedbackOnboarding/feedbackOnboardingLayout.tsx b/static/app/components/feedback/feedbackOnboarding/feedbackOnboardingLayout.tsx index f6b0422d7c91..b74d834c56c7 100644 --- a/static/app/components/feedback/feedbackOnboarding/feedbackOnboardingLayout.tsx +++ b/static/app/components/feedback/feedbackOnboarding/feedbackOnboardingLayout.tsx @@ -1,5 +1,6 @@ import {useMemo, useState} from 'react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import {Stack} from '@sentry/scraps/layout'; @@ -14,7 +15,6 @@ import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/types'; import {useSourcePackageRegistries} from 'sentry/components/onboarding/gettingStartedDoc/useSourcePackageRegistries'; import {useUrlPlatformOptions} from 'sentry/components/onboarding/platformOptionsControl'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {useApi} from 'sentry/utils/useApi'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -37,7 +37,8 @@ export function FeedbackOnboardingLayout({ const {isPending: isLoadingRegistry, data: registryData} = useSourcePackageRegistries(organization); const selectedOptions = useUrlPlatformOptions(docsConfig.platformOptions); - const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const urlPrefix = useSelector(ConfigStore.store, state => state.urlPrefix); const {introduction, steps} = useMemo(() => { const doc = docsConfig[configType] ?? docsConfig.onboarding; diff --git a/static/app/components/feedback/feedbackOnboarding/sidebar.tsx b/static/app/components/feedback/feedbackOnboarding/sidebar.tsx index 106d9525b855..f4f0e3779394 100644 --- a/static/app/components/feedback/feedbackOnboarding/sidebar.tsx +++ b/static/app/components/feedback/feedbackOnboarding/sidebar.tsx @@ -1,6 +1,7 @@ import type {ReactNode} from 'react'; import {Fragment, useEffect, useMemo, useState} from 'react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import {parseAsStringLiteral, useQueryState} from 'nuqs'; import {PlatformIcon} from 'platformicons'; @@ -40,7 +41,6 @@ import { OnboardingDrawerKey, OnboardingDrawerStore, } from 'sentry/stores/onboardingDrawerStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {PlatformKey} from 'sentry/types/platform'; import type {Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -49,7 +49,7 @@ import {useOrganization} from 'sentry/utils/useOrganization'; export function useFeedbackOnboardingDrawer() { const organization = useOrganization(); - const currentPanel = useLegacyStore(OnboardingDrawerStore); + const currentPanel = useSelector(OnboardingDrawerStore.store); const isActive = currentPanel === OnboardingDrawerKey.FEEDBACK_ONBOARDING; const hasProjectAccess = organization.access.includes('project:read'); diff --git a/static/app/components/feedback/list/mailboxPicker.tsx b/static/app/components/feedback/list/mailboxPicker.tsx index 06c6033c854f..826c1fecdfd6 100644 --- a/static/app/components/feedback/list/mailboxPicker.tsx +++ b/static/app/components/feedback/list/mailboxPicker.tsx @@ -1,3 +1,5 @@ +import {useSelector} from '@tanstack/react-store'; + import {Badge} from '@sentry/scraps/badge'; import {Flex} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; @@ -9,7 +11,6 @@ import {useMailboxCounts} from 'sentry/components/feedback/list/useMailboxCounts import type {Mailbox} from 'sentry/components/feedback/useMailbox'; import {t, tct} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {useOrganization} from 'sentry/utils/useOrganization'; interface Props { @@ -20,7 +21,7 @@ interface Props { export function MailboxPicker({onChange, value}: Props) { const organization = useOrganization(); const {data} = useMailboxCounts({organization}); - const {isSelfHosted} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); const {areAiFeaturesAllowed} = useOrganizationSeerSetup(); const hasSpamFeature = organization.features.includes('user-feedback-spam-ingest'); diff --git a/static/app/components/feedbackButton/useFeedbackSDKIntegration.tsx b/static/app/components/feedbackButton/useFeedbackSDKIntegration.tsx index e4414870d078..2e79306da74a 100644 --- a/static/app/components/feedbackButton/useFeedbackSDKIntegration.tsx +++ b/static/app/components/feedbackButton/useFeedbackSDKIntegration.tsx @@ -1,9 +1,9 @@ import {useMemo} from 'react'; import type * as Sentry from '@sentry/react'; +import {useSelector} from '@tanstack/react-store'; import {t} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {useAsyncSDKIntegrationStore} from 'sentry/views/app/asyncSDKIntegrationProvider'; export type FeedbackIntegration = NonNullable>; @@ -14,19 +14,19 @@ export function useFeedbackSDKIntegration(): { defaultOptions: NonNullable; feedback: FeedbackIntegration | undefined; } { - const config = useLegacyStore(ConfigStore); + const colorMode = useSelector(ConfigStore.store, state => state.theme); const {state} = useAsyncSDKIntegrationStore(); const feedback = state.Feedback as FeedbackIntegration | undefined; const defaultOptions = useMemo((): NonNullable => { return { - colorScheme: config.theme === 'dark' ? ('dark' as const) : ('light' as const), + colorScheme: colorMode === 'dark' ? ('dark' as const) : ('light' as const), submitButtonLabel: t('Send Feedback'), messagePlaceholder: t('What did you expect?'), formTitle: t('Give Feedback'), }; - }, [config.theme]); + }, [colorMode]); return {feedback, defaultOptions}; } diff --git a/static/app/components/frontendVersionContext.tsx b/static/app/components/frontendVersionContext.tsx index 7d7afd761202..17da1c98e55e 100644 --- a/static/app/components/frontendVersionContext.tsx +++ b/static/app/components/frontendVersionContext.tsx @@ -1,8 +1,8 @@ import {createContext, useContext, useEffect, useState} from 'react'; +import {useSelector} from '@tanstack/react-store'; import {DEPLOY_PREVIEW_CONFIG, NODE_ENV} from 'sentry/constants'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {useApiQuery} from 'sentry/utils/queryClient'; @@ -76,7 +76,7 @@ const FrontendVersionContext = createContext({ * and on tab focus to detect if the frontend version has changed. */ export function FrontendVersionProvider({children, releaseVersion}: Props) { - const {sentryMode} = useLegacyStore(ConfigStore); + const sentryMode = useSelector(ConfigStore.store, state => state.sentryMode); // Version checking only applies to production SAAS builds const isSaas = sentryMode === 'SAAS'; diff --git a/static/app/components/idBadge/teamBadge.tsx b/static/app/components/idBadge/teamBadge.tsx index b3878b47e850..732ed83e4bb8 100644 --- a/static/app/components/idBadge/teamBadge.tsx +++ b/static/app/components/idBadge/teamBadge.tsx @@ -1,5 +1,6 @@ +import {useSelector} from '@tanstack/react-store'; + import {TeamStore} from 'sentry/stores/teamStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Team} from 'sentry/types/organization'; import {BadgeDisplayName} from './badgeDisplayName'; @@ -14,7 +15,7 @@ export interface TeamBadgeProps extends BaseBadgeProps { } function TeamBadge({hideOverflow = true, team, ...props}: TeamBadgeProps) { - const {teams} = useLegacyStore(TeamStore); + const teams = useSelector(TeamStore.store, state => state.teams); // Get the most up-to-date team from the store const resolvedTeam = teams.find(t => t.id === team.id) ?? team; diff --git a/static/app/components/modals/dataWidgetViewerModal.spec.tsx b/static/app/components/modals/dataWidgetViewerModal.spec.tsx index b7f6b41fedba..e12f9b9d09b1 100644 --- a/static/app/components/modals/dataWidgetViewerModal.spec.tsx +++ b/static/app/components/modals/dataWidgetViewerModal.spec.tsx @@ -105,6 +105,7 @@ describe('Modals -> DataWidgetViewerModal', () => { let initialData: InitialData; let initialDataWithFlag: InitialData; beforeEach(() => { + ProjectsStore.reset(); const projects = [ProjectFixture()]; const organization = OrganizationFixture({ features: ['discover-query'], @@ -155,7 +156,6 @@ describe('Modals -> DataWidgetViewerModal', () => { afterEach(() => { MockApiClient.clearMockResponses(); - ProjectsStore.reset(); }); describe('Errors Widgets', () => { diff --git a/static/app/components/onboarding/gettingStartedDoc/onboardingLayout.tsx b/static/app/components/onboarding/gettingStartedDoc/onboardingLayout.tsx index efed4c5db59b..a6c64774aa2a 100644 --- a/static/app/components/onboarding/gettingStartedDoc/onboardingLayout.tsx +++ b/static/app/components/onboarding/gettingStartedDoc/onboardingLayout.tsx @@ -1,5 +1,6 @@ import {Fragment, useCallback, useEffect, useMemo} from 'react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import {Stack} from '@sentry/scraps/layout'; import {ExternalLink} from '@sentry/scraps/link'; @@ -35,7 +36,6 @@ import {ProductSelection} from 'sentry/components/onboarding/productSelection'; import {OverrideOrDefault} from 'sentry/components/overrideOrDefault'; import {t} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {PlatformKey} from 'sentry/types/platform'; import type {Project, ProjectKey} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -83,7 +83,8 @@ export function OnboardingLayout({ useSourcePackageRegistries(organization); const selectedOptions = useUrlPlatformOptions(docsConfig.platformOptions); const {platformOptions} = docsConfig; - const {urlPrefix, isSelfHosted} = useLegacyStore(ConfigStore); + const urlPrefix = useSelector(ConfigStore.store, state => state.urlPrefix); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); const { introduction, diff --git a/static/app/components/onboarding/gettingStartedDoc/utils/useCurrentProjectState.ts b/static/app/components/onboarding/gettingStartedDoc/utils/useCurrentProjectState.ts index e41d90687029..fcb2bbf3286a 100644 --- a/static/app/components/onboarding/gettingStartedDoc/utils/useCurrentProjectState.ts +++ b/static/app/components/onboarding/gettingStartedDoc/utils/useCurrentProjectState.ts @@ -1,10 +1,10 @@ import {useCallback, useEffect, useMemo, useState} from 'react'; +import {useSelector} from '@tanstack/react-store'; import partition from 'lodash/partition'; import {parseAsString, useQueryState} from 'nuqs'; import {PageFiltersStore} from 'sentry/components/pageFilters/store'; import type {OnboardingDrawerKey} from 'sentry/stores/onboardingDrawerStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {PlatformKey} from 'sentry/types/platform'; import type {Project} from 'sentry/types/project'; import {getSelectedProjectList} from 'sentry/utils/project/useSelectedProjectsHaveField'; @@ -24,7 +24,8 @@ export function useCurrentProjectState({ allPlatforms, }: Props) { const {projects, initiallyLoaded: projectsLoaded} = useProjects(); - const {selection, isReady} = useLegacyStore(PageFiltersStore); + const selection = useSelector(PageFiltersStore.store, state => state.selection); + const isReady = useSelector(PageFiltersStore.store, state => state.isReady); const [projectId] = useQueryState('project', parseAsString); const isActive = currentPanel === targetPanel; diff --git a/static/app/components/onboarding/scm/useScmProjectCreation.spec.tsx b/static/app/components/onboarding/scm/useScmProjectCreation.spec.tsx index 49f252f00737..d683059859b7 100644 --- a/static/app/components/onboarding/scm/useScmProjectCreation.spec.tsx +++ b/static/app/components/onboarding/scm/useScmProjectCreation.spec.tsx @@ -49,6 +49,8 @@ describe('useScmProjectCreation', () => { } beforeEach(() => { + TeamStore.reset(); + ProjectsStore.reset(); TeamStore.loadInitialData([adminTeam]); ProjectsStore.loadInitialData([]); MockApiClient.addMockResponse({ @@ -67,8 +69,6 @@ describe('useScmProjectCreation', () => { }); afterEach(() => { - TeamStore.reset(); - ProjectsStore.reset(); MockApiClient.clearMockResponses(); }); diff --git a/static/app/components/onboarding/scm/useScmProjectDetails.spec.tsx b/static/app/components/onboarding/scm/useScmProjectDetails.spec.tsx index eb78172bc2e0..b88af7bd4ad4 100644 --- a/static/app/components/onboarding/scm/useScmProjectDetails.spec.tsx +++ b/static/app/components/onboarding/scm/useScmProjectDetails.spec.tsx @@ -93,6 +93,7 @@ describe('useScmProjectDetails', () => { } beforeEach(() => { + TeamStore.reset(); // useCreateNotificationAction queries messaging integrations on mount. MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/integrations/`, @@ -102,7 +103,6 @@ describe('useScmProjectDetails', () => { }); afterEach(() => { - TeamStore.reset(); MockApiClient.clearMockResponses(); jest.restoreAllMocks(); }); diff --git a/static/app/components/pageFilters/container.spec.tsx b/static/app/components/pageFilters/container.spec.tsx index c7cb2ff01703..df264086c0e7 100644 --- a/static/app/components/pageFilters/container.spec.tsx +++ b/static/app/components/pageFilters/container.spec.tsx @@ -29,6 +29,7 @@ describe('PageFiltersContainer', () => { }); beforeEach(() => { + PageFiltersStore.reset(); MockApiClient.clearMockResponses(); ProjectsStore.loadInitialData(projects); OrganizationStore.onUpdate(organization); @@ -44,7 +45,6 @@ describe('PageFiltersContainer', () => { afterEach(() => { jest.clearAllMocks(); - PageFiltersStore.reset(); }); it('does not update router if org in URL params is different than org in context/props', () => { diff --git a/static/app/components/pageFilters/environment/environmentPageFilter.spec.tsx b/static/app/components/pageFilters/environment/environmentPageFilter.spec.tsx index 9363ff00eec2..950b03ad420e 100644 --- a/static/app/components/pageFilters/environment/environmentPageFilter.spec.tsx +++ b/static/app/components/pageFilters/environment/environmentPageFilter.spec.tsx @@ -18,6 +18,7 @@ const projects = [ describe('EnvironmentPageFilter', () => { beforeEach(() => { + PageFiltersStore.reset(); OrganizationStore.init(); PageFiltersStore.init(); @@ -31,8 +32,6 @@ describe('EnvironmentPageFilter', () => { ProjectsStore.loadInitialData(projects); }); - afterEach(() => PageFiltersStore.reset()); - it('renders & handles single selection', async () => { const {router} = render(, { organization, diff --git a/static/app/components/pageFilters/project/projectPageFilter.spec.tsx b/static/app/components/pageFilters/project/projectPageFilter.spec.tsx index fcc1e1384747..3c78f7eea04b 100644 --- a/static/app/components/pageFilters/project/projectPageFilter.spec.tsx +++ b/static/app/components/pageFilters/project/projectPageFilter.spec.tsx @@ -26,6 +26,7 @@ const projects = [ describe('ProjectPageFilter', () => { beforeEach(() => { + PageFiltersStore.reset(); OrganizationStore.init(); PageFiltersStore.init(); @@ -39,8 +40,6 @@ describe('ProjectPageFilter', () => { ProjectsStore.loadInitialData(projects); }); - afterEach(() => PageFiltersStore.reset()); - it('renders & handles single selection', async () => { const {router} = render(, { organization, diff --git a/static/app/components/pageFilters/store.spec.tsx b/static/app/components/pageFilters/store.spec.tsx index 7bc55ee851f1..bf7e925abf51 100644 --- a/static/app/components/pageFilters/store.spec.tsx +++ b/static/app/components/pageFilters/store.spec.tsx @@ -53,7 +53,7 @@ describe('PageFiltersStore', () => { it('does not update if projects has same value', async () => { const triggerSpy = jest.fn(); - const unsubscribe = PageFiltersStore.subscribe(triggerSpy); + const {unsubscribe} = PageFiltersStore.store.subscribe(triggerSpy); PageFiltersStore.updateProjects([1], []); await waitFor(() => PageFiltersStore.getState().selection.projects[0] === 1); @@ -114,7 +114,7 @@ describe('PageFiltersStore', () => { const end = new Date(now + 1000); const triggerSpy = jest.fn(); - const unsubscribe = PageFiltersStore.subscribe(triggerSpy); + const {unsubscribe} = PageFiltersStore.store.subscribe(triggerSpy); PageFiltersStore.updateDateTime({end, start, period: null, utc: null}); await waitFor(() => PageFiltersStore.getState().selection.datetime.start === start); @@ -140,7 +140,7 @@ describe('PageFiltersStore', () => { it('does not update if environments has same value', async () => { const triggerSpy = jest.fn(); - const unsubscribe = PageFiltersStore.subscribe(triggerSpy); + const {unsubscribe} = PageFiltersStore.store.subscribe(triggerSpy); PageFiltersStore.updateEnvironments(['alpha']); await waitFor( () => PageFiltersStore.getState().selection.environments[0] === 'alpha' diff --git a/static/app/components/pageFilters/store.tsx b/static/app/components/pageFilters/store.tsx index 0256c559f603..705fed70bf82 100644 --- a/static/app/components/pageFilters/store.tsx +++ b/static/app/components/pageFilters/store.tsx @@ -1,6 +1,7 @@ +import {Store} from '@tanstack/react-store'; + import type {PageFilterAdjustments} from 'sentry/components/pageFilters/adjustments'; import {getDefaultPageFilterSelection} from 'sentry/components/pageFilters/constants'; -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {PageFilters, PinnedPageFilter, PageFilterDatetime} from 'sentry/types/core'; import {valueIsEqual} from 'sentry/utils/object/valueIsEqual'; @@ -80,17 +81,17 @@ interface PageFiltersState { shouldPersist: boolean; } -const store = createLegacyStore({ +const store = new Store({ isReady: false, selection: getDefaultPageFilterSelection(), - pinnedFilters: new Set(), + pinnedFilters: new Set(), shouldPersist: true, adjustments: {}, }); function getResetState(selection?: PageFilters): PageFiltersState { return { - ...store.getState(), + ...store.state, isReady: false, selection: selection || getDefaultPageFilterSelection(), pinnedFilters: new Set(), @@ -99,14 +100,18 @@ function getResetState(selection?: PageFilters): PageFiltersState { } export const PageFiltersStore = { - ...store, + store, + + getState() { + return store.state; + }, init() { - PageFiltersStore.reset(store.getState().selection); + PageFiltersStore.reset(store.state.selection); }, reset(selection?: PageFilters) { - store.resetState(getResetState(selection)); + store.setState(() => getResetState(selection)); }, /** @@ -128,7 +133,7 @@ export const PageFiltersStore = { }, onReset() { - store.setState(() => getResetState()); + PageFiltersStore.reset(); }, updatePersistence(shouldPersist: boolean) { @@ -143,7 +148,7 @@ export const PageFiltersStore = { filter: F, adjustment: NonNullable ) { - const {adjustments} = store.getState(); + const {adjustments} = store.state; if (valueIsEqual(adjustments[filter], adjustment, true)) { return; } @@ -158,7 +163,7 @@ export const PageFiltersStore = { projects: PageFilters['projects'] = [], environments: null | string[] = null ) { - const state = store.getState(); + const state = store.state; if (valueIsEqual(state.selection.projects, projects)) { return; } @@ -178,7 +183,7 @@ export const PageFiltersStore = { }, updateDateTime(newDateTime: PageFilterDatetime) { - const state = store.getState(); + const state = store.state; if (datetimeHasSameValue(state.selection.datetime, newDateTime)) { return; } @@ -191,7 +196,7 @@ export const PageFiltersStore = { }, updateEnvironments(environments: string[] | null) { - const state = store.getState(); + const state = store.state; if (valueIsEqual(state.selection.environments, environments)) { return; } @@ -204,7 +209,7 @@ export const PageFiltersStore = { }, pin(filter: PinnedPageFilter, pin: boolean) { - const newPinnedFilters = new Set(store.getState().pinnedFilters); + const newPinnedFilters = new Set(store.state.pinnedFilters); if (pin) { newPinnedFilters.add(filter); } else { diff --git a/static/app/components/pageFilters/usePageFilters.tsx b/static/app/components/pageFilters/usePageFilters.tsx index fa2729b775a9..d066898099f0 100644 --- a/static/app/components/pageFilters/usePageFilters.tsx +++ b/static/app/components/pageFilters/usePageFilters.tsx @@ -1,9 +1,10 @@ +import {useSelector} from '@tanstack/react-store'; + import {PageFiltersStore} from 'sentry/components/pageFilters/store'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; /** * Custom hook that returns the state of page filters */ export function usePageFilters() { - return useLegacyStore(PageFiltersStore); + return useSelector(PageFiltersStore.store); } diff --git a/static/app/components/performanceOnboarding/sidebar.tsx b/static/app/components/performanceOnboarding/sidebar.tsx index 0a2ef29dfcf5..a3b74df7af80 100644 --- a/static/app/components/performanceOnboarding/sidebar.tsx +++ b/static/app/components/performanceOnboarding/sidebar.tsx @@ -1,5 +1,6 @@ import {Fragment, useEffect, useMemo, useRef, useState} from 'react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import HighlightTopRightPattern from 'sentry-images/pattern/highlight-top-right.svg'; @@ -29,7 +30,6 @@ import { OnboardingDrawerKey, OnboardingDrawerStore, } from 'sentry/stores/onboardingDrawerStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {pulsingIndicatorStyles} from 'sentry/styles/pulsingIndicator'; import type {Project} from 'sentry/types/project'; import {useApi} from 'sentry/utils/useApi'; @@ -54,7 +54,7 @@ function decodeProjectIds(projectIds: unknown): string[] | null { export function usePerformanceOnboardingDrawer() { const organization = useOrganization(); - const currentPanel = useLegacyStore(OnboardingDrawerStore); + const currentPanel = useSelector(OnboardingDrawerStore.store); const isActive = currentPanel === OnboardingDrawerKey.PERFORMANCE_ONBOARDING; const hasProjectAccess = organization.access.includes('project:read'); const initialPathname = useRef(null); @@ -91,7 +91,7 @@ function SidebarContent() { const [currentProject, setCurrentProject] = useState(undefined); - const {selection} = useLegacyStore(PageFiltersStore); + const selection = useSelector(PageFiltersStore.store, state => state.selection); const {projectsWithoutFirstTransactionEvent, projectsForOnboarding} = filterProjects(projects); @@ -232,7 +232,8 @@ function SidebarContent() { function OnboardingContent({currentProject}: {currentProject: Project}) { const api = useApi(); const organization = useOrganization(); - const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const urlPrefix = useSelector(ConfigStore.store, state => state.urlPrefix); const firstIssue = useEventWaiter({ eventType: 'transaction', organization, diff --git a/static/app/components/platformPicker.tsx b/static/app/components/platformPicker.tsx index 792021b32396..43d35723be65 100644 --- a/static/app/components/platformPicker.tsx +++ b/static/app/components/platformPicker.tsx @@ -3,6 +3,7 @@ import {css} from '@emotion/react'; import styled from '@emotion/styled'; import {IconClose} from '@sentry/icons/close'; import {IconProject} from '@sentry/icons/project'; +import {useSelector} from '@tanstack/react-store'; import debounce from 'lodash/debounce'; import {PlatformIcon} from 'platformicons'; @@ -23,7 +24,6 @@ import { import {otherPlatform, allPlatforms as platforms} from 'sentry/data/platforms'; import {t, tct} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Organization} from 'sentry/types/organization'; import type {PlatformIntegration} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -80,7 +80,7 @@ export function PlatformPicker({ visibleSelection = true, loading = false, }: PlatformPickerProps) { - const {isSelfHosted} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); const [category, setCategory] = useState(defaultCategory ?? categoryList[0]!.id); const [filter, setFilter] = useState( diff --git a/static/app/components/profiling/profilingOnboardingSidebar.tsx b/static/app/components/profiling/profilingOnboardingSidebar.tsx index 4fc0d7735efe..da4511c8e71e 100644 --- a/static/app/components/profiling/profilingOnboardingSidebar.tsx +++ b/static/app/components/profiling/profilingOnboardingSidebar.tsx @@ -1,5 +1,6 @@ import {Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import partition from 'lodash/partition'; import {CompactSelect} from '@sentry/scraps/compactSelect'; @@ -31,7 +32,6 @@ import { OnboardingDrawerKey, OnboardingDrawerStore, } from 'sentry/stores/onboardingDrawerStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Organization} from 'sentry/types/organization'; import type {PlatformIntegration, Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -59,7 +59,7 @@ const PROFILING_ONBOARDING_STEPS = [ export function useProfilingOnboardingDrawer() { const organization = useOrganization(); - const currentPanel = useLegacyStore(OnboardingDrawerStore); + const currentPanel = useSelector(OnboardingDrawerStore.store); const isActive = currentPanel === OnboardingDrawerKey.PROFILING_ONBOARDING; const hasProjectAccess = organization.access.includes('project:read'); const initialPathname = useRef(null); @@ -273,7 +273,8 @@ function ProfilingOnboardingContent(props: ProfilingOnboardingContentProps) { const {isPending: isLoadingRegistry, data: registryData} = useSourcePackageRegistries(organization); - const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const urlPrefix = useSelector(ConfigStore.store, state => state.urlPrefix); if (isLoading) { return ; diff --git a/static/app/components/projects/missingProjectMembership.spec.tsx b/static/app/components/projects/missingProjectMembership.spec.tsx index a3bb05c65a4a..d1744c5ad49b 100644 --- a/static/app/components/projects/missingProjectMembership.spec.tsx +++ b/static/app/components/projects/missingProjectMembership.spec.tsx @@ -8,8 +8,11 @@ import {MissingProjectMembership} from 'sentry/components/projects/missingProjec import {TeamStore} from 'sentry/stores/teamStore'; describe('MissingProjectMembership', () => { - afterEach(() => { + beforeEach(() => { TeamStore.reset(); + }); + + afterEach(() => { MockApiClient.clearMockResponses(); }); diff --git a/static/app/components/projects/missingProjectMembership.tsx b/static/app/components/projects/missingProjectMembership.tsx index 79b3fd07dbfb..4def1fd12bdb 100644 --- a/static/app/components/projects/missingProjectMembership.tsx +++ b/static/app/components/projects/missingProjectMembership.tsx @@ -1,6 +1,7 @@ import {useMemo, useState} from 'react'; import styled from '@emotion/styled'; import {IconFlag} from '@sentry/icons/flag'; +import {useSelector} from '@tanstack/react-store'; import {Button} from '@sentry/scraps/button'; import {CompactSelect} from '@sentry/scraps/compactSelect'; @@ -13,7 +14,6 @@ import {EmptyMessage} from 'sentry/components/emptyMessage'; import {Panel} from 'sentry/components/panels/panel'; import {t} from 'sentry/locale'; import {TeamStore} from 'sentry/stores/teamStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {useApi} from 'sentry/utils/useApi'; @@ -26,8 +26,8 @@ interface JoinTeamActionProps { function JoinTeamAction({teamSlug, organization}: JoinTeamActionProps) { const api = useApi({persistInFlight: true}); const [isLoading, setIsLoading] = useState(false); - const teamStoreData = useLegacyStore(TeamStore); - const selectedTeam = teamStoreData.teams.find(team => team.slug === teamSlug); + const storeTeams = useSelector(TeamStore.store, state => state.teams); + const selectedTeam = storeTeams.find(team => team.slug === teamSlug); const handleJoinTeam = async () => { setIsLoading(true); @@ -77,13 +77,13 @@ export function MissingProjectMembership({ project, }: MissingProjectMembershipProps) { const [selectedTeam, setSelectedTeam] = useState(null); - const teamStoreData = useLegacyStore(TeamStore); + const storeTeams = useSelector(TeamStore.store, state => state.teams); const teamOptions = useMemo(() => { const request: string[] = []; const pending: string[] = []; const pendingTeams = new Set( - teamStoreData.teams.filter(tm => tm.isPending).map(tm => tm.slug) + storeTeams.filter(tm => tm.isPending).map(tm => tm.slug) ); project?.teams?.forEach(({slug}) => { if (pendingTeams.has(slug)) { @@ -111,7 +111,7 @@ export function MissingProjectMembership({ })), }, ]; - }, [organization.features, project, teamStoreData.teams]); + }, [organization.features, project, storeTeams]); const hasTeams = !!project?.teams?.length; return ( diff --git a/static/app/components/replaysOnboarding/replayOnboardingLayout.tsx b/static/app/components/replaysOnboarding/replayOnboardingLayout.tsx index c6d865ea6aad..c399dd0d5c2c 100644 --- a/static/app/components/replaysOnboarding/replayOnboardingLayout.tsx +++ b/static/app/components/replaysOnboarding/replayOnboardingLayout.tsx @@ -1,5 +1,6 @@ import {useMemo, useState} from 'react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import {Stack} from '@sentry/scraps/layout'; @@ -14,7 +15,6 @@ import {useSourcePackageRegistries} from 'sentry/components/onboarding/gettingSt import {useUrlPlatformOptions} from 'sentry/components/onboarding/platformOptionsControl'; import {ReplayConfigToggle} from 'sentry/components/replaysOnboarding/replayConfigToggle'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {useApi} from 'sentry/utils/useApi'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -35,7 +35,8 @@ export function ReplayOnboardingLayout({ const selectedOptions = useUrlPlatformOptions(docsConfig.platformOptions); const [mask, setMask] = useState(true); const [block, setBlock] = useState(true); - const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const urlPrefix = useSelector(ConfigStore.store, state => state.urlPrefix); const {introduction, steps} = useMemo(() => { const doc = docsConfig[configType] ?? docsConfig.onboarding; diff --git a/static/app/components/replaysOnboarding/sidebar.tsx b/static/app/components/replaysOnboarding/sidebar.tsx index 54950c2d1715..67f610871059 100644 --- a/static/app/components/replaysOnboarding/sidebar.tsx +++ b/static/app/components/replaysOnboarding/sidebar.tsx @@ -1,6 +1,7 @@ import type {ReactNode} from 'react'; import {Fragment, useEffect, useMemo, useState} from 'react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import omit from 'lodash/omit'; import {parseAsStringLiteral, useQueryState} from 'nuqs'; import {PlatformIcon} from 'platformicons'; @@ -38,14 +39,13 @@ import { OnboardingDrawerKey, OnboardingDrawerStore, } from 'sentry/stores/onboardingDrawerStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {PlatformKey} from 'sentry/types/platform'; import type {Project} from 'sentry/types/project'; import {useOrganization} from 'sentry/utils/useOrganization'; export function useReplaysOnboardingDrawer() { const organization = useOrganization(); - const currentPanel = useLegacyStore(OnboardingDrawerStore); + const currentPanel = useSelector(OnboardingDrawerStore.store); const isActive = currentPanel === OnboardingDrawerKey.REPLAYS_ONBOARDING; const hasProjectAccess = organization.access.includes('project:read'); diff --git a/static/app/components/seer/projectAddRepoModal/projectAddRepoModal.spec.tsx b/static/app/components/seer/projectAddRepoModal/projectAddRepoModal.spec.tsx index 46222289799e..fb72d12f76f9 100644 --- a/static/app/components/seer/projectAddRepoModal/projectAddRepoModal.spec.tsx +++ b/static/app/components/seer/projectAddRepoModal/projectAddRepoModal.spec.tsx @@ -55,13 +55,13 @@ describe('ProjectAddRepoModal', () => { } beforeEach(() => { + ProjectsStore.reset(); ProjectsStore.loadInitialData([project]); mockEndpoints(); }); afterEach(() => { MockApiClient.clearMockResponses(); - ProjectsStore.reset(); }); function openAddRepoModal(org = organization) { diff --git a/static/app/components/seer/projectTable/seerProjectTable.spec.tsx b/static/app/components/seer/projectTable/seerProjectTable.spec.tsx index 3e6c019d90ce..695c626cf69b 100644 --- a/static/app/components/seer/projectTable/seerProjectTable.spec.tsx +++ b/static/app/components/seer/projectTable/seerProjectTable.spec.tsx @@ -87,13 +87,13 @@ describe('SeerProjectTable', () => { } beforeEach(() => { + ProjectsStore.reset(); ProjectsStore.loadInitialData([project]); mockBaseEndpoints(); }); afterEach(() => { MockApiClient.clearMockResponses(); - ProjectsStore.reset(); jest.restoreAllMocks(); }); diff --git a/static/app/components/themeAndStyleProvider.tsx b/static/app/components/themeAndStyleProvider.tsx index d1a3b295c4df..485651e9673c 100644 --- a/static/app/components/themeAndStyleProvider.tsx +++ b/static/app/components/themeAndStyleProvider.tsx @@ -2,13 +2,13 @@ import {Fragment, lazy, useRef} from 'react'; import {createPortal} from 'react-dom'; import createCache from '@emotion/cache'; import {CacheProvider, ThemeProvider} from '@emotion/react'; +import {useSelector} from '@tanstack/react-store'; import {darkTheme, lightTheme} from '@sentry/scraps/theme'; import {printConsoleBanner} from 'sentry/bootstrap/printConsoleBanner'; import {NODE_ENV} from 'sentry/constants'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {GlobalStyles} from 'sentry/styles/global'; const SentryComponentInspector = @@ -39,9 +39,9 @@ cache.compat = true; * Also injects the sentry GlobalStyles . */ export function ThemeAndStyleProvider({children}: Props) { - const config = useLegacyStore(ConfigStore); + const colorMode = useSelector(ConfigStore.store, state => state.theme); - const theme = config.theme === 'dark' ? darkTheme : lightTheme; + const theme = colorMode === 'dark' ? darkTheme : lightTheme; const didPrintBanner = useRef(false); // oxlint-disable-next-line react/refs @@ -56,7 +56,7 @@ export function ThemeAndStyleProvider({children}: Props) { {children} {createPortal( - + , document.head diff --git a/static/app/components/updatedEmptyState.tsx b/static/app/components/updatedEmptyState.tsx index 97277d5c42b9..346c9067fde2 100644 --- a/static/app/components/updatedEmptyState.tsx +++ b/static/app/components/updatedEmptyState.tsx @@ -1,5 +1,6 @@ import {useEffect} from 'react'; import {useTheme} from '@emotion/react'; +import {useSelector} from '@tanstack/react-store'; import waitingForEventImg from 'sentry-images/spot/waiting-for-event.svg'; @@ -27,7 +28,6 @@ import {useLoadGettingStarted} from 'sentry/components/onboarding/gettingStarted import {allPlatforms as platforms} from 'sentry/data/platforms'; import {t, tct} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {pulsingIndicatorStyles} from 'sentry/styles/pulsingIndicator'; import type {Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -92,7 +92,8 @@ export default function UpdatedEmptyState({project}: {project?: Project}) { const {isPending: isLoadingRegistry, data: registryData} = useSourcePackageRegistries(organization); - const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const urlPrefix = useSelector(ConfigStore.store, state => state.urlPrefix); const currentPlatformKey = project?.platform ?? 'other'; const currentPlatform = platforms.find(p => p.id === currentPlatformKey)!; diff --git a/static/app/debug/notifications/components/debugNotificationsHeader.tsx b/static/app/debug/notifications/components/debugNotificationsHeader.tsx index a83b5501340a..5011a7bdd473 100644 --- a/static/app/debug/notifications/components/debugNotificationsHeader.tsx +++ b/static/app/debug/notifications/components/debugNotificationsHeader.tsx @@ -2,6 +2,7 @@ import styled from '@emotion/styled'; import {IconGithub} from '@sentry/icons/github'; import {IconLink} from '@sentry/icons/link'; import {IconMoon} from '@sentry/icons/moon'; +import {useSelector} from '@tanstack/react-store'; import {Button, LinkButton} from '@sentry/scraps/button'; import {Flex} from '@sentry/scraps/layout'; @@ -10,7 +11,6 @@ import {Heading} from '@sentry/scraps/text'; import {DebugNotificationsSearch} from 'sentry/debug/notifications/components/debugNotificationsSearch'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {useLocation} from 'sentry/utils/useLocation'; export function DebugNotificationsHeader() { @@ -76,8 +76,7 @@ const HeaderGrid = styled('div')` `; function ThemeSwitcher() { - const config = useLegacyStore(ConfigStore); - const isDark = config.theme === 'dark'; + const isDark = useSelector(ConfigStore.store, state => state.theme === 'dark'); const handleClick = () => { ConfigStore.set('theme', isDark ? 'light' : 'dark'); diff --git a/static/app/stores/IssueListCacheStore.tsx b/static/app/stores/IssueListCacheStore.tsx index 8545fb3c2dc4..ccc0c2806c4d 100644 --- a/static/app/stores/IssueListCacheStore.tsx +++ b/static/app/stores/IssueListCacheStore.tsx @@ -1,6 +1,6 @@ +import {Store} from '@tanstack/react-store'; import isEqual from 'lodash/isEqual'; -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {Group} from 'sentry/types/group'; /** @@ -34,21 +34,29 @@ interface IssueListCacheState { // 30 seconds const CACHE_EXPIRATION = 30 * 1000; -const store = createLegacyStore(null); +const store = new Store(null); export const IssueListCacheStore = { - ...store, + store, + + getState() { + return store.state; + }, reset() { - store.resetState(null); + store.setState(() => null); }, save(params: LooseParamsType, data: IssueListCache) { - store.resetState({params, cache: data, expiration: Date.now() + CACHE_EXPIRATION}); + store.setState(() => ({ + params, + cache: data, + expiration: Date.now() + CACHE_EXPIRATION, + })); }, getFromCache(params: LooseParamsType) { - const state = store.getState(); + const state = store.state; if (state && state.expiration > Date.now() && isEqual(state.params, params)) { return state.cache; } @@ -57,9 +65,7 @@ export const IssueListCacheStore = { }, markGroupAsSeen(groupId: string) { - const matchingGroup = store - .getState() - ?.cache.groups.find(group => group.id === groupId); + const matchingGroup = store.state?.cache.groups.find(group => group.id === groupId); if (matchingGroup) { matchingGroup.hasSeen = true; } diff --git a/static/app/stores/configStore.tsx b/static/app/stores/configStore.tsx index fd07aee07d7a..5fae9c0f960a 100644 --- a/static/app/stores/configStore.tsx +++ b/static/app/stores/configStore.tsx @@ -1,25 +1,29 @@ +import {Store} from '@tanstack/react-store'; import moment from 'moment-timezone'; -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {Config} from 'sentry/types/system'; // When the app is booted we will _immediately_ hydrate the config store, // effecively ensuring this is not empty. -const store = createLegacyStore({} as Config); +const store = new Store({} as Config); export const ConfigStore = { - ...store, + store, + + getState() { + return store.state; + }, init(): void { - store.resetState({} as Config); + store.setState(() => ({}) as Config); }, get(key: K): Config[K] { - return store.getState()[key]; + return store.state[key]; }, set(key: K, value: Config[K]): void { - store.setState(() => ({...store.getState(), [key]: value})); + store.setState(() => ({...store.state, [key]: value})); }, loadInitialData(config: Config): void { diff --git a/static/app/stores/createLegacyStore.tsx b/static/app/stores/createLegacyStore.tsx deleted file mode 100644 index 669e10732da5..000000000000 --- a/static/app/stores/createLegacyStore.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import {Store} from '@tanstack/react-store'; - -/** - * Wraps a TanStack Store with the `getState`/`subscribe` shape our stores - * expose, and lets callers replace the state without notifying subscribers. - * - * Our stores have always reset their state silently, and many callers (mostly tests - * resetting stores in `afterEach` while components are still mounted) rely on - * that. Use `resetState` for those writes and `setState` for everything else. - */ -export function createLegacyStore(initialState: T) { - const store = new Store(initialState); - let silent = false; - - const source = { - get: () => store.state, - subscribe: (listener: (state: T) => void) => - store.subscribe(state => { - if (!silent) { - listener(state); - } - }), - }; - - return { - /** - * Pass to `useSelector` from `@tanstack/react-store` - */ - source, - - getState: () => store.state, - - setState: (updater: (prev: T) => T) => store.setState(updater), - - /** - * Returns an unsubscribe function - */ - subscribe(listener: (state: T) => void) { - return source.subscribe(listener).unsubscribe; - }, - - /** - * Replaces the state without notifying subscribers - */ - resetState(state: T) { - silent = true; - try { - store.setState(() => state); - } finally { - silent = false; - } - }, - }; -} diff --git a/static/app/stores/demoWalkthroughStore.tsx b/static/app/stores/demoWalkthroughStore.tsx index 14203caac482..bb0c5020ce95 100644 --- a/static/app/stores/demoWalkthroughStore.tsx +++ b/static/app/stores/demoWalkthroughStore.tsx @@ -1,4 +1,5 @@ -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; +import {Store} from '@tanstack/react-store'; + import {OnboardingTaskKey} from 'sentry/types/onboarding'; type State = { @@ -6,7 +7,7 @@ type State = { sidebarGuideAnchor: boolean; }; -const store = createLegacyStore({ +const store = new Store({ issueGuideAnchor: false, sidebarGuideAnchor: false, }); @@ -16,7 +17,11 @@ const store = createLegacyStore({ * Side-effects (like reading/writing to cookies) are done in associated actionCreators */ export const DemoWalkthroughStore = { - ...store, + store, + + getState() { + return store.state; + }, activateGuideAnchor(task: OnboardingTaskKey) { switch (task) { @@ -33,9 +38,9 @@ export const DemoWalkthroughStore = { get(guide: string) { switch (guide) { case 'issue': - return store.getState().issueGuideAnchor; + return store.state.issueGuideAnchor; case 'sidebar': - return store.getState().sidebarGuideAnchor; + return store.state.sidebarGuideAnchor; default: return false; } diff --git a/static/app/stores/groupStore.spec.tsx b/static/app/stores/groupStore.spec.tsx index 92dd4acdd48b..850560bceafc 100644 --- a/static/app/stores/groupStore.spec.tsx +++ b/static/app/stores/groupStore.spec.tsx @@ -118,7 +118,7 @@ describe('GroupStore', () => { beforeEach(() => { listener = jest.fn(); - unsubscribe = GroupStore.subscribe(listener); + unsubscribe = GroupStore.store.subscribe(listener).unsubscribe; GroupStore.items = [g('1'), g('2'), g('3')]; }); afterEach(() => { @@ -181,7 +181,7 @@ describe('GroupStore', () => { describe('update methods', () => { beforeEach(() => { listener = jest.fn(); - unsubscribe = GroupStore.subscribe(listener); + unsubscribe = GroupStore.store.subscribe(listener).unsubscribe; GroupStore.items = [g('1'), g('2'), g('3')]; }); afterEach(() => { diff --git a/static/app/stores/groupStore.tsx b/static/app/stores/groupStore.tsx index 08a915e230d3..cb16952484bb 100644 --- a/static/app/stores/groupStore.tsx +++ b/static/app/stores/groupStore.tsx @@ -1,7 +1,8 @@ +import {Store} from '@tanstack/react-store'; + import {toast} from '@sentry/scraps/toast'; import {t} from 'sentry/locale'; -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {BaseGroup, Group, GroupStats} from 'sentry/types/group'; import {toArray} from 'sentry/utils/array/toArray'; import {parseApiError} from 'sentry/utils/parseApiError'; @@ -48,10 +49,14 @@ function mergePendingChanges( ); } -const store = createLegacyStore([]); +const store = new Store([]); export const GroupStore = { - ...store, + store, + + getState() { + return store.state; + }, /** * Does not include pending changes * TODO: Remove mutation and replace state with items @@ -68,7 +73,7 @@ export const GroupStore = { GroupStore.pendingChanges = new Map(); GroupStore.items = []; GroupStore.statuses = {}; - store.resetState([]); + store.setState(() => []); }, loadInitialData(items: Item[]) { @@ -174,7 +179,7 @@ export const GroupStore = { }, getAllItems(): readonly Item[] { - return store.getState(); + return store.state; }, onAssignTo(_changeId: string, itemId: string, _data: any) { diff --git a/static/app/stores/guideStore.tsx b/static/app/stores/guideStore.tsx index 98beadba56a8..9857f5d08418 100644 --- a/static/app/stores/guideStore.tsx +++ b/static/app/stores/guideStore.tsx @@ -1,8 +1,9 @@ +import {Store} from '@tanstack/react-store'; + import {getGuidesContent} from 'sentry/components/assistant/getGuidesContent'; import type {Guide, GuidesServerData} from 'sentry/components/assistant/types'; import {getOverride} from 'sentry/overrideRegistry'; import {ConfigStore} from 'sentry/stores/configStore'; -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import {ModalStore} from 'sentry/stores/modalStore'; import type {Organization} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -77,7 +78,7 @@ function isForceEnabled() { return window.location.hash === '#assistant'; } -const store = createLegacyStore({...defaultState}); +const store = new Store({...defaultState}); let unsubscribeModalStore: (() => void) | null = null; @@ -87,25 +88,29 @@ function onURLChange() { } export const GuideStore = { - ...store, + store, + + getState() { + return store.state; + }, /** * @deprecated Use `getState()` */ get state() { - return store.getState(); + return store.state; }, init() { - store.resetState({...defaultState, forceShow: isForceEnabled()}); + store.setState(() => ({...defaultState, forceShow: isForceEnabled()})); window.addEventListener('load', onURLChange, false); // Guides will show above modals, but are not interactable because // of the focus trap, so we force them to be hidden while a modal is open. - unsubscribeModalStore = ModalStore.subscribe(modalState => { + unsubscribeModalStore = ModalStore.store.subscribe(modalState => { GuideStore.setForceHide(typeof modalState.renderer === 'function'); - }); + }).unsubscribe; }, teardown() { @@ -153,7 +158,7 @@ export const GuideStore = { }, closeGuide(dismissed?: boolean) { - const {currentGuide, guides} = store.getState(); + const {currentGuide, guides} = store.state; const newGuides = guides.map(guide => { // update the current guide seen to true or all guides @@ -175,16 +180,16 @@ export const GuideStore = { }, nextStep() { - store.setState(prev => ({...prev, currentStep: store.getState().currentStep + 1})); + store.setState(prev => ({...prev, currentStep: store.state.currentStep + 1})); }, registerAnchor(target: string) { - store.getState().anchors.add(target); + store.state.anchors.add(target); GuideStore.updateCurrentGuide(); }, unregisterAnchor(target: string) { - store.getState().anchors.delete(target); + store.state.anchors.delete(target); GuideStore.updateCurrentGuide(); }, @@ -199,13 +204,13 @@ export const GuideStore = { } trackAnalytics('assistant.guide_cued', { - organization: store.getState().orgId, + organization: store.state.orgId, guide, }); }, updatePrevGuide(nextGuide: Guide | null) { - const {prevGuide} = store.getState(); + const {prevGuide} = store.state; if (!nextGuide) { return; } @@ -225,7 +230,7 @@ export const GuideStore = { * - Otherwise show the guide */ updateCurrentGuide(dismissed?: boolean) { - const {anchors, guides, forceShow} = store.getState(); + const {anchors, guides, forceShow} = store.state; let guideOptions = guides .sort(guidePrioritySort) @@ -266,7 +271,7 @@ export const GuideStore = { : null; GuideStore.updatePrevGuide(nextGuide); - const {currentGuide, currentStep} = store.getState(); + const {currentGuide, currentStep} = store.state; store.setState(prev => ({ ...prev, currentGuide: nextGuide, diff --git a/static/app/stores/modalStore.tsx b/static/app/stores/modalStore.tsx index 7b67780e9584..fc98a0eddfe9 100644 --- a/static/app/stores/modalStore.tsx +++ b/static/app/stores/modalStore.tsx @@ -1,7 +1,7 @@ +import {Store} from '@tanstack/react-store'; import type {FocusTrap} from 'focus-trap'; import type {ModalOptions, ModalRenderProps} from 'sentry/actionCreators/modal'; -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; type Renderer = (renderProps: ModalRenderProps) => React.ReactNode; @@ -12,43 +12,39 @@ type State = { triggerElement?: HTMLElement | null; }; -const store = createLegacyStore({renderer: null, options: {}}); +const store = new Store({renderer: null, options: {}}); export const ModalStore = { - ...store, + store, + + getState() { + return store.state; + }, init() { ModalStore.reset(); }, reset() { - store.resetState({ + store.setState(prev => ({ + ...prev, renderer: null, options: {}, - focusTrap: store.getState().focusTrap, - }); + triggerElement: undefined, + })); }, closeModal() { - store.setState(() => ({ - renderer: null, - options: {}, - focusTrap: store.getState().focusTrap, - })); + ModalStore.reset(); }, openModal(renderer: Renderer, options: ModalOptions) { const triggerElement = document.activeElement instanceof HTMLElement ? document.activeElement : null; - store.setState(() => ({ - renderer, - options, - focusTrap: store.getState().focusTrap, - triggerElement, - })); + store.setState(prev => ({...prev, renderer, options, triggerElement})); }, setFocusTrap(focusTrap: FocusTrap) { - store.resetState({...store.getState(), focusTrap}); + store.setState(prev => ({...prev, focusTrap})); }, }; diff --git a/static/app/stores/onboardingDrawerStore.tsx b/static/app/stores/onboardingDrawerStore.tsx index b63cf3140b05..2ffef29f0c4c 100644 --- a/static/app/stores/onboardingDrawerStore.tsx +++ b/static/app/stores/onboardingDrawerStore.tsx @@ -1,4 +1,4 @@ -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; +import {Store} from '@tanstack/react-store'; type ActivePanelType = Readonly; @@ -13,14 +13,18 @@ export enum OnboardingDrawerKey { FEATURE_FLAG_ONBOARDING = 'flag_onboarding', } -const store = createLegacyStore(''); +const store = new Store(''); /** * This store is used to hold local user preferences * Side-effects (like reading/writing to cookies) are done in associated actionCreators */ export const OnboardingDrawerStore = { - ...store, + store, + + getState() { + return store.state; + }, init() {}, @@ -29,7 +33,7 @@ export const OnboardingDrawerStore = { }, toggle(panel: OnboardingDrawerKey) { - if (store.getState() === panel) { + if (store.state === panel) { OnboardingDrawerStore.close(); } else { OnboardingDrawerStore.open(panel); diff --git a/static/app/stores/organizationStore.tsx b/static/app/stores/organizationStore.tsx index 96a1911e3fae..7e4a3d035430 100644 --- a/static/app/stores/organizationStore.tsx +++ b/static/app/stores/organizationStore.tsx @@ -1,5 +1,6 @@ +import {Store} from '@tanstack/react-store'; + import {ORGANIZATION_FETCH_ERROR_TYPES} from 'sentry/constants'; -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {Organization} from 'sentry/types/organization'; import type {RequestError} from 'sentry/utils/requestError/requestError'; import {callSetOrganizationCallback} from 'sentry/utils/routeAnalytics/setOrganizationCallback'; @@ -20,13 +21,13 @@ const initialState: State = { errorType: null, }; -const store = createLegacyStore(initialState); +const store = new Store(initialState); function onUpdate(org: Organization, options?: {replace: true}): void; function onUpdate(org: Partial, options?: {replace?: false}): void; function onUpdate(updatedOrg: Partial, {replace = false} = {}) { const organization = ( - replace ? updatedOrg : {...store.getState().organization, ...updatedOrg} + replace ? updatedOrg : {...store.state.organization, ...updatedOrg} ) as Organization; store.setState(() => ({ loading: false, @@ -40,7 +41,11 @@ function onUpdate(updatedOrg: Partial, {replace = false} = {}) { } export const OrganizationStore = { - ...store, + store, + + getState() { + return store.state; + }, init() { OrganizationStore.reset(); @@ -84,6 +89,6 @@ export const OrganizationStore = { }, get() { - return store.getState(); + return store.state; }, }; diff --git a/static/app/stores/organizationsStore.tsx b/static/app/stores/organizationsStore.tsx index beac6a831129..fed0909c835b 100644 --- a/static/app/stores/organizationsStore.tsx +++ b/static/app/stores/organizationsStore.tsx @@ -1,4 +1,5 @@ -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; +import {Store} from '@tanstack/react-store'; + import type {OrganizationSummary} from 'sentry/types/organization'; interface State { @@ -6,18 +7,22 @@ interface State { organizations: OrganizationSummary[]; } -const store = createLegacyStore({organizations: [], loaded: false}); +const store = new Store({organizations: [], loaded: false}); export const OrganizationsStore = { - ...store, + store, + + getState() { + return store.state; + }, init() { - store.resetState({organizations: [], loaded: false}); + store.setState(() => ({organizations: [], loaded: false})); }, onUpdate(org: Partial) { let match = false; - const newOrgs = [...store.getState().organizations]; + const newOrgs = [...store.state.organizations]; newOrgs.forEach((existing, idx) => { if (existing.id === org.id) { newOrgs[idx] = {...existing, ...org}; @@ -46,23 +51,23 @@ export const OrganizationsStore = { }, get(slug: string) { - return store.getState().organizations.find(item => item.slug === slug); + return store.state.organizations.find(item => item.slug === slug); }, getAll() { - return store.getState().organizations; + return store.state.organizations; }, remove(slug: string) { store.setState(prev => ({ ...prev, - organizations: store.getState().organizations.filter(item => slug !== item.slug), + organizations: store.state.organizations.filter(item => slug !== item.slug), })); }, addOrReplace(item: OrganizationSummary) { let match = false; - const newOrgs = [...store.getState().organizations]; + const newOrgs = [...store.state.organizations]; newOrgs.forEach((existing, idx) => { if (existing.id === item.id) { newOrgs[idx] = {...existing, ...item}; diff --git a/static/app/stores/projectsStore.tsx b/static/app/stores/projectsStore.tsx index c1cd14abc5eb..3355b53995e6 100644 --- a/static/app/stores/projectsStore.tsx +++ b/static/app/stores/projectsStore.tsx @@ -1,5 +1,6 @@ +import {Store} from '@tanstack/react-store'; + import {clearQueryCache} from 'sentry/appQueryClient'; -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {Team} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; @@ -8,7 +9,7 @@ type State = { projects: Project[]; }; -const store = createLegacyStore({projects: [], loading: true}); +const store = new Store({projects: [], loading: true}); function setProjects(projects: Project[]) { store.setState(prev => ({...prev, projects})); @@ -18,19 +19,23 @@ function setProjects(projects: Project[]) { function removeTeamFromProject(teamSlug: string, project: Project) { const newTeams = project.teams.filter(({slug}) => slug !== teamSlug); const newProject = {...project, teams: newTeams}; - setProjects(store.getState().projects.map(p => (p.id === project.id ? newProject : p))); + setProjects(store.state.projects.map(p => (p.id === project.id ? newProject : p))); clearQueryCache(); } export const ProjectsStore = { - ...store, + store, + + getState() { + return store.state; + }, init() { ProjectsStore.reset(); }, reset() { - store.resetState({projects: [], loading: true}); + store.setState(() => ({projects: [], loading: true})); }, loadInitialData(items: Project[]) { @@ -49,9 +54,8 @@ export const ProjectsStore = { const newProject = {...prevProject, slug: newSlug}; setProjects( - store - .getState() - .projects.map(project => (project.slug === prevSlug ? newProject : project)) + store.state.projects + .map(project => (project.slug === prevSlug ? newProject : project)) .toSorted((a, b) => a.slug.localeCompare(b.slug)) ); clearQueryCache(); @@ -59,10 +63,7 @@ export const ProjectsStore = { onCreateSuccess(project: Project) { setProjects( - store - .getState() - .projects.concat([project]) - .sort((a, b) => a.slug.localeCompare(b.slug)) + store.state.projects.concat([project]).sort((a, b) => a.slug.localeCompare(b.slug)) ); clearQueryCache(); }, @@ -75,9 +76,7 @@ export const ProjectsStore = { } const newProject = {...project, ...data}; - setProjects( - store.getState().projects.map(p => (p.id === project.id ? newProject : p)) - ); + setProjects(store.state.projects.map(p => (p.id === project.id ? newProject : p))); clearQueryCache(); }, @@ -86,7 +85,7 @@ export const ProjectsStore = { if (!project) { return; } - setProjects(store.getState().projects.filter(p => p.id !== project.id)); + setProjects(store.state.projects.filter(p => p.id !== project.id)); clearQueryCache(); }, @@ -97,9 +96,9 @@ export const ProjectsStore = { */ onDeleteTeam(teamSlug: string) { // Look for team in all projects - const projects = store - .getState() - .projects.filter(({teams}) => teams.find(({slug}) => slug === teamSlug)); + const projects = store.state.projects.filter(({teams}) => + teams.find(({slug}) => slug === teamSlug) + ); projects.forEach(project => removeTeamFromProject(teamSlug, project)); clearQueryCache(); @@ -125,21 +124,19 @@ export const ProjectsStore = { } const newProject = {...project, teams: [...project.teams, team]}; - setProjects( - store.getState().projects.map(p => (p.id === project.id ? newProject : p)) - ); + setProjects(store.state.projects.map(p => (p.id === project.id ? newProject : p))); clearQueryCache(); }, isLoading() { - return store.getState().loading; + return store.state.loading; }, getById(id?: string) { - return store.getState().projects.find(project => project.id === id); + return store.state.projects.find(project => project.id === id); }, getBySlug(slug?: string) { - return store.getState().projects.find(project => project.slug === slug); + return store.state.projects.find(project => project.slug === slug); }, }; diff --git a/static/app/stores/tagStore.spec.tsx b/static/app/stores/tagStore.spec.tsx index 05bea2339f84..7ef052e26c87 100644 --- a/static/app/stores/tagStore.spec.tsx +++ b/static/app/stores/tagStore.spec.tsx @@ -10,7 +10,7 @@ describe('TagStore', () => { describe('loadTagsSuccess()', () => { it('should add a new tag with empty values and trigger the new addition', () => { const listener = jest.fn(); - const unsubscribe = TagStore.subscribe(listener); + const {unsubscribe} = TagStore.store.subscribe(listener); TagStore.loadTagsSuccess([ { diff --git a/static/app/stores/tagStore.tsx b/static/app/stores/tagStore.tsx index e3ff225fc08b..2763053cd05d 100644 --- a/static/app/stores/tagStore.tsx +++ b/static/app/stores/tagStore.tsx @@ -1,13 +1,18 @@ -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; +import {Store} from '@tanstack/react-store'; + import type {Tag, TagCollection} from 'sentry/types/group'; -const store = createLegacyStore({}); +const store = new Store({}); export const TagStore = { - ...store, + store, + + getState() { + return store.state; + }, init() { - store.resetState({}); + store.setState(() => ({})); }, reset() { @@ -28,7 +33,7 @@ export const TagStore = { }; } - const previousState = store.getState(); + const previousState = store.state; // We will iterate through the previous tags in reverse so that previously // added tags are carried over first. We rely on browser implementation // of Object.keys() to return keys in insertion order. diff --git a/static/app/stores/teamStore.tsx b/static/app/stores/teamStore.tsx index 9c4522e4dfad..241daba1f04a 100644 --- a/static/app/stores/teamStore.tsx +++ b/static/app/stores/teamStore.tsx @@ -1,4 +1,5 @@ -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; +import {Store} from '@tanstack/react-store'; + import type {Team} from 'sentry/types/organization'; import {defined} from 'sentry/utils/defined'; @@ -20,15 +21,13 @@ const initialState: State = { cursor: null, }; -const store = createLegacyStore(initialState); +const store = new Store(initialState); function updateTeams(teams: Team[]) { - const teamIdMap = store - .getState() - .teams.reduce((acc: Record, team: Team) => { - acc[team.id] = team; - return acc; - }, {}); + const teamIdMap = store.state.teams.reduce((acc: Record, team: Team) => { + acc[team.id] = team; + return acc; + }, {}); // Replace or insert new user teams teams.reduce((acc: Record, userTeam: Team) => { @@ -40,7 +39,11 @@ function updateTeams(teams: Team[]) { } export const TeamStore = { - ...store, + store, + + getState() { + return store.state; + }, initialized: false, init() { @@ -48,12 +51,12 @@ export const TeamStore = { }, reset() { - store.resetState(initialState); + store.setState(() => initialState); }, setTeams(teams: Team[], hasMore?: boolean | null, cursor?: string | null) { TeamStore.initialized = true; - const state = store.getState(); + const state = store.state; store.setState(() => ({ teams, loadedUserTeams: defined(hasMore) ? !hasMore : state.loadedUserTeams, @@ -80,7 +83,7 @@ export const TeamStore = { return; } - const {teams} = store.getState(); + const {teams} = store.state; const item = TeamStore.getBySlug(itemId); if (!item) { @@ -108,7 +111,7 @@ export const TeamStore = { }, onRemoveSuccess(slug: string) { - TeamStore.setTeams(store.getState().teams.filter(team => team.slug !== slug)); + TeamStore.setTeams(store.state.teams.filter(team => team.slug !== slug)); ProjectsStore.onDeleteTeam(slug); }, @@ -117,16 +120,14 @@ export const TeamStore = { }, getById(id: string) { - return ( - store.getState().teams.find(item => item.id.toString() === id.toString()) || null - ); + return store.state.teams.find(item => item.id.toString() === id.toString()) || null; }, getBySlug(slug: string) { - return store.getState().teams.find(item => item.slug === slug) || null; + return store.state.teams.find(item => item.slug === slug) || null; }, getAll() { - return store.getState().teams; + return store.state.teams; }, }; diff --git a/static/app/stores/useLegacyStore.spec.tsx b/static/app/stores/useLegacyStore.spec.tsx deleted file mode 100644 index cddee91ca87a..000000000000 --- a/static/app/stores/useLegacyStore.spec.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import {TeamFixture} from 'sentry-fixture/team'; - -import {act, renderHook} from 'sentry-test/reactTestingLibrary'; - -import {TeamStore} from 'sentry/stores/teamStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; - -describe('useLegacyStore', () => { - const team = TeamFixture(); - - beforeEach(() => TeamStore.reset()); - - it('should update on change to store', () => { - const {result} = renderHook(useLegacyStore, {initialProps: TeamStore}); - - expect(result.current.teams).toEqual([]); - - act(() => TeamStore.loadInitialData([team])); - - expect(result.current.teams).toEqual([team]); - }); -}); diff --git a/static/app/stores/useLegacyStore.tsx b/static/app/stores/useLegacyStore.tsx deleted file mode 100644 index d02ca93e7e71..000000000000 --- a/static/app/stores/useLegacyStore.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import {useSelector} from '@tanstack/react-store'; - -import type {createLegacyStore} from 'sentry/stores/createLegacyStore'; - -type LegacyStore = Pick>, 'source' | 'getState'>; - -/** - * Returns the state of a store and re-renders when it changes. - * - * ```tsx - * const teams = useLegacyStore(TeamStore); - * ``` - */ -export function useLegacyStore( - store: S -): ReturnType { - return useSelector(store.source); -} diff --git a/static/app/stories/theme.tsx b/static/app/stories/theme.tsx index a331a07f45d1..9f5d3a0f1820 100644 --- a/static/app/stories/theme.tsx +++ b/static/app/stories/theme.tsx @@ -1,14 +1,13 @@ import {IconMoon} from '@sentry/icons/moon'; +import {useSelector} from '@tanstack/react-store'; import {Button} from '@sentry/scraps/button'; import {t} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; export function ThemeSwitcher() { - const config = useLegacyStore(ConfigStore); - const isDark = config.theme === 'dark'; + const isDark = useSelector(ConfigStore.store, state => state.theme === 'dark'); const handleClick = () => { ConfigStore.set('theme', isDark ? 'light' : 'dark'); diff --git a/static/app/utils/membersAndTeams/useAssignedSearchValues.tsx b/static/app/utils/membersAndTeams/useAssignedSearchValues.tsx index 52127ce18ea8..e0dd2ba1900a 100644 --- a/static/app/utils/membersAndTeams/useAssignedSearchValues.tsx +++ b/static/app/utils/membersAndTeams/useAssignedSearchValues.tsx @@ -1,17 +1,17 @@ import {useMemo} from 'react'; import {IconStar} from '@sentry/icons/star'; import {IconUser} from '@sentry/icons/user'; +import {useSelector} from '@tanstack/react-store'; import {ItemType, type SearchGroup} from 'sentry/components/searchBar/types'; import {escapeTagValue} from 'sentry/components/searchBar/utils'; import {t} from 'sentry/locale'; import {TeamStore} from 'sentry/stores/teamStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {useMembers} from 'sentry/utils/members/useMembers'; import {getUsername} from 'sentry/utils/membersAndTeams/userUtils'; export function useAssignedSearchValues(): SearchGroup[] { - const {teams} = useLegacyStore(TeamStore); + const teams = useSelector(TeamStore.store, state => state.teams); const {data: members = []} = useMembers(); const assignedValues: SearchGroup[] = useMemo(() => { diff --git a/static/app/utils/profiling/flamegraph/flamegraphThemeProvider.tsx b/static/app/utils/profiling/flamegraph/flamegraphThemeProvider.tsx index 28409aee6744..b53d6e068004 100644 --- a/static/app/utils/profiling/flamegraph/flamegraphThemeProvider.tsx +++ b/static/app/utils/profiling/flamegraph/flamegraphThemeProvider.tsx @@ -1,9 +1,9 @@ import {createContext, useCallback, useMemo, useState} from 'react'; import {useTheme} from '@emotion/react'; +import {useSelector} from '@tanstack/react-store'; import cloneDeep from 'lodash/cloneDeep'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {FlamegraphTheme} from 'sentry/utils/profiling/flamegraph/flamegraphTheme'; import { makeDarkFlamegraphTheme, @@ -29,7 +29,7 @@ function FlamegraphThemeProvider( props: FlamegraphThemeProviderProps ): React.ReactElement { const theme = useTheme(); - const {theme: colorMode} = useLegacyStore(ConfigStore); + const colorMode = useSelector(ConfigStore.store, state => state.theme); const [mutation, setMutation] = useState(null); const addModifier = useCallback((cb: FlamegraphThemeMutationCallback) => { diff --git a/static/app/utils/timeSeries/useFetchEventsTimeSeries.spec.tsx b/static/app/utils/timeSeries/useFetchEventsTimeSeries.spec.tsx index 6fc5601ac0da..600efa8410da 100644 --- a/static/app/utils/timeSeries/useFetchEventsTimeSeries.spec.tsx +++ b/static/app/utils/timeSeries/useFetchEventsTimeSeries.spec.tsx @@ -13,6 +13,7 @@ describe('useFetchEventsTimeSeries', () => { const organization = OrganizationFixture(); beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState( PageFiltersFixture({ datetime: { @@ -28,7 +29,6 @@ describe('useFetchEventsTimeSeries', () => { }); afterEach(() => { - PageFiltersStore.reset(); jest.resetAllMocks(); }); diff --git a/static/app/utils/useProjects.tsx b/static/app/utils/useProjects.tsx index a57f41db67c1..53c620e4369b 100644 --- a/static/app/utils/useProjects.tsx +++ b/static/app/utils/useProjects.tsx @@ -1,9 +1,9 @@ import {useEffect, useRef, useState} from 'react'; +import {useSelector} from '@tanstack/react-store'; import uniqBy from 'lodash/uniqBy'; import type {Client} from 'sentry/api'; import {ProjectsStore} from 'sentry/stores/projectsStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {AvatarProject, Project} from 'sentry/types/project'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {parseLinkHeader} from 'sentry/utils/parseLinkHeader'; @@ -162,16 +162,17 @@ export function useProjects({limit, slugs, orgId: propOrgId}: Options = {}) { const api = useApi(); const organization = useOrganization({allowNull: true}); - const store = useLegacyStore(ProjectsStore); + const storeProjects = useSelector(ProjectsStore.store, state => state.projects); + const storeLoading = useSelector(ProjectsStore.store, state => state.loading); const orgId = propOrgId ?? organization?.slug; - const storeSlugs = new Set(store.projects.map(t => t.slug)); + const storeSlugs = new Set(storeProjects.map(t => t.slug)); const slugsToLoad = slugs?.filter(slug => !storeSlugs.has(slug)) ?? []; const shouldLoadSlugs = slugsToLoad.length > 0; const [state, setState] = useState({ - initiallyLoaded: !store.loading && !shouldLoadSlugs, + initiallyLoaded: !storeLoading && !shouldLoadSlugs, fetching: shouldLoadSlugs, hasMore: null, lastSearch: null, @@ -211,7 +212,7 @@ export function useProjects({limit, slugs, orgId: propOrgId}: Options = {}) { }); // Note the order of uniqBy: we prioritize project data recently fetched over previously cached data - const fetchedProjects = uniqBy([...results, ...store.projects], ({slug}) => slug); + const fetchedProjects = uniqBy([...results, ...storeProjects], ({slug}) => slug); ProjectsStore.loadInitialData(fetchedProjects); setState(prev => ({ @@ -227,7 +228,7 @@ export function useProjects({limit, slugs, orgId: propOrgId}: Options = {}) { setState(prev => ({ ...prev, fetching: false, - initiallyLoaded: !store.loading, + initiallyLoaded: !storeLoading, fetchError: err as RequestError, })); } @@ -258,10 +259,10 @@ export function useProjects({limit, slugs, orgId: propOrgId}: Options = {}) { cursor, }); - const fetchedProjects = uniqBy([...store.projects, ...results], ({slug}) => slug); + const fetchedProjects = uniqBy([...storeProjects, ...results], ({slug}) => slug); // Only update the store if we have more items - if (fetchedProjects.length > store.projects.length) { + if (fetchedProjects.length > storeProjects.length) { ProjectsStore.loadInitialData(fetchedProjects); } @@ -291,7 +292,7 @@ export function useProjects({limit, slugs, orgId: propOrgId}: Options = {}) { // Update initiallyLoaded when we finish loading within the projectStore useEffect(() => { - const storeLoaded = !store.loading; + const storeLoaded = !storeLoading; if (state.initiallyLoaded === storeLoaded) { return; @@ -304,20 +305,20 @@ export function useProjects({limit, slugs, orgId: propOrgId}: Options = {}) { // eslint-disable-next-line react-you-might-not-need-an-effect/no-derived-state setState(prev => ({...prev, initiallyLoaded: storeLoaded})); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [store.loading]); + }, [storeLoading]); const {initiallyLoaded, fetching, fetchError, hasMore} = state; const filteredProjects = slugs - ? store.projects.filter(t => slugs.includes(t.slug)) - : store.projects; + ? storeProjects.filter(t => slugs.includes(t.slug)) + : storeProjects; const placeholders = slugsToLoad.map(slug => ({slug})); const result: Result = { projects: filteredProjects, placeholders, - fetching: fetching || store.loading, + fetching: fetching || storeLoading, initiallyLoaded, fetchError, hasMore, diff --git a/static/app/utils/useServiceIncidents.tsx b/static/app/utils/useServiceIncidents.tsx index e152d2ef2848..42ef5587f6dd 100644 --- a/static/app/utils/useServiceIncidents.tsx +++ b/static/app/utils/useServiceIncidents.tsx @@ -1,7 +1,7 @@ import {useQuery} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {StatusPageComponent, StatuspageIncident} from 'sentry/types/system'; interface UseServiceIncidentsOptions { @@ -26,7 +26,7 @@ export function useServiceIncidents({ includeResolved, componentFilter, }: UseServiceIncidentsOptions = {}) { - const {statuspage} = useLegacyStore(ConfigStore); + const statuspage = useSelector(ConfigStore.store, state => state.statuspage); const {api_host, id} = statuspage ?? {}; return useQuery({ diff --git a/static/app/utils/useTags.tsx b/static/app/utils/useTags.tsx index bb8213436808..820800530fca 100644 --- a/static/app/utils/useTags.tsx +++ b/static/app/utils/useTags.tsx @@ -1,7 +1,8 @@ +import {useSelector} from '@tanstack/react-store'; + import {TagStore} from 'sentry/stores/tagStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {TagCollection} from 'sentry/types/group'; export function useTags(): TagCollection { - return useLegacyStore(TagStore); + return useSelector(TagStore.store); } diff --git a/static/app/utils/useTeams.tsx b/static/app/utils/useTeams.tsx index d15cec738860..0d3ba41e983d 100644 --- a/static/app/utils/useTeams.tsx +++ b/static/app/utils/useTeams.tsx @@ -1,11 +1,11 @@ import {useCallback, useEffect, useMemo, useState} from 'react'; +import {useSelector} from '@tanstack/react-store'; import uniqBy from 'lodash/uniqBy'; import {fetchUserTeams} from 'sentry/actionCreators/teams'; import type {Client} from 'sentry/api'; import {OrganizationStore} from 'sentry/stores/organizationStore'; import {TeamStore} from 'sentry/stores/teamStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Team} from 'sentry/types/organization'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser'; @@ -170,8 +170,8 @@ async function fetchTeams( */ export function useTeams({limit, slugs, provideUserTeams}: Options = {}) { const api = useApi(); - const {organization} = useLegacyStore(OrganizationStore); - const store = useLegacyStore(TeamStore); + const organization = useSelector(OrganizationStore.store, state => state.organization); + const store = useSelector(TeamStore.store); const orgId = organization?.slug; diff --git a/static/app/utils/useTeamsById.tsx b/static/app/utils/useTeamsById.tsx index 9d9bf5c4dad4..e64bcf8f7fe2 100644 --- a/static/app/utils/useTeamsById.tsx +++ b/static/app/utils/useTeamsById.tsx @@ -1,8 +1,8 @@ import {useEffect, useMemo} from 'react'; import {useQuery} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import {TeamStore} from 'sentry/stores/teamStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Team} from 'sentry/types/organization'; import {apiOptions} from 'sentry/utils/api/apiOptions'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -49,7 +49,8 @@ function teamsApiOptions(orgSlug: string, property: 'id' | 'slug', values: strin */ export function useTeamsById(options: UseTeamOptions = {}): UseTeamsResult { const organization = useOrganization({allowNull: true}); - const storeState = useLegacyStore(TeamStore); + const storeTeams = useSelector(TeamStore.store, state => state.teams); + const storeLoading = useSelector(TeamStore.store, state => state.loading); const teamQueryValues = useMemo<{ property: 'id' | 'slug'; @@ -68,12 +69,12 @@ export function useTeamsById(options: UseTeamOptions = {}): UseTeamsResult { const missingValues = teamQueryValues?.values ? Array.from(teamQueryValues.values).filter( - value => !storeState.teams.some(team => team[teamQueryValues.property] === value) + value => !storeTeams.some(team => team[teamQueryValues.property] === value) ) : []; // Wait until the store has loaded to start fetching - const shouldConsiderFetching = !!organization?.slug && !storeState.loading; + const shouldConsiderFetching = !!organization?.slug && !storeLoading; // Only fetch if there are missing teams const hasMissing = missingValues.length > 0; const queryEnabled = shouldConsiderFetching && hasMissing; @@ -101,15 +102,15 @@ export function useTeamsById(options: UseTeamOptions = {}): UseTeamsResult { const teams = useMemo(() => { return teamQueryValues - ? storeState.teams.filter(team => + ? storeTeams.filter(team => teamQueryValues.values?.has(team[teamQueryValues.property]) ) - : storeState.teams; - }, [storeState.teams, teamQueryValues]); + : storeTeams; + }, [storeTeams, teamQueryValues]); return { teams, - isLoading: queryEnabled ? isPending : storeState.loading, + isLoading: queryEnabled ? isPending : storeLoading, isError: queryEnabled ? isError : null, }; } diff --git a/static/app/utils/useUser.tsx b/static/app/utils/useUser.tsx index f60959910873..7e1d1fb7d3ac 100644 --- a/static/app/utils/useUser.tsx +++ b/static/app/utils/useUser.tsx @@ -1,5 +1,6 @@ +import {useSelector} from '@tanstack/react-store'; + import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {User} from 'sentry/types/user'; /** @@ -8,10 +9,10 @@ import type {User} from 'sentry/types/user'; export function useUser(): Readonly { // Intentional exception to accessing the deprecated field as we want to // deter users from consuming the user differently than through the hook. - const config = useLegacyStore(ConfigStore); + const user = useSelector(ConfigStore.store, state => state.user); // @TODO: Return a readonly type as a mechanism to deter users from mutating the // user directly. That said, this provides basic type safety and no runtime safety // as there are still plenty of ways to mutate the user. The runtime safe way of // enforcing this would be via Object.freeze. - return config.user; + return user; } diff --git a/static/app/utils/useUserTeams.tsx b/static/app/utils/useUserTeams.tsx index 3dc99caf2b50..f6da9bf21c8e 100644 --- a/static/app/utils/useUserTeams.tsx +++ b/static/app/utils/useUserTeams.tsx @@ -1,8 +1,8 @@ import {useEffect, useMemo} from 'react'; +import {useSelector} from '@tanstack/react-store'; import {OrganizationStore} from 'sentry/stores/organizationStore'; import {TeamStore} from 'sentry/stores/teamStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Team} from 'sentry/types/organization'; import type {ApiQueryKey} from 'sentry/utils/api/apiQueryKey'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; @@ -33,13 +33,15 @@ function buildUserTeamsQueryKey(orgSlug: string): ApiQueryKey { * ``` */ export function useUserTeams(): UseTeamsResult { - const {organization} = useLegacyStore(OrganizationStore); - const storeState = useLegacyStore(TeamStore); + const organization = useSelector(OrganizationStore.store, state => state.organization); + const storeTeams = useSelector(TeamStore.store, state => state.teams); + const storeLoading = useSelector(TeamStore.store, state => state.loading); + const loadedUserTeams = useSelector(TeamStore.store, state => state.loadedUserTeams); // Wait until the store has loaded to start fetching - const shouldConsiderFetching = !!organization?.slug && !storeState.loading; + const shouldConsiderFetching = !!organization?.slug && !storeLoading; // Only fetch if there are missing teams - const hasMissing = !storeState.loadedUserTeams; + const hasMissing = !loadedUserTeams; const queryEnabled = shouldConsiderFetching && hasMissing; const { @@ -61,14 +63,12 @@ export function useUserTeams(): UseTeamsResult { const isSuperuser = isActiveSuperuser(); const isOrgOwner = organization?.access.includes('org:admin'); const teams = useMemo(() => { - return isSuperuser || isOrgOwner - ? storeState.teams - : storeState.teams.filter(t => t.isMember); - }, [isSuperuser, isOrgOwner, storeState.teams]); + return isSuperuser || isOrgOwner ? storeTeams : storeTeams.filter(t => t.isMember); + }, [isSuperuser, isOrgOwner, storeTeams]); return { teams, - isLoading: queryEnabled ? isPending : storeState.loading, + isLoading: queryEnabled ? isPending : storeLoading, isError: queryEnabled ? isError : null, }; } diff --git a/static/app/views/app/index.tsx b/static/app/views/app/index.tsx index 57fd8bc077ca..928ab2fa1cb8 100644 --- a/static/app/views/app/index.tsx +++ b/static/app/views/app/index.tsx @@ -1,6 +1,7 @@ import {lazy, Suspense, useCallback, useEffect} from 'react'; import {Outlet} from 'react-router'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import {GlobalModal} from '@sentry/scraps/modal'; @@ -16,7 +17,6 @@ import {getOverride} from 'sentry/overrideRegistry'; import {ConfigStore} from 'sentry/stores/configStore'; import {GuideStore} from 'sentry/stores/guideStore'; import {OrganizationsStore} from 'sentry/stores/organizationsStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Config} from 'sentry/types/system'; import {isValidOrgSlug} from 'sentry/utils/isValidOrgSlug'; import {onRenderCallback, Profiler} from 'sentry/utils/performanceForSentry'; @@ -41,8 +41,9 @@ const BeaconConsent = lazy(() => import('sentry/views/beaconConsent')); function AppAlerts() { const {addAlert} = useGlobalAlerts(); const api = useApi(); - const config = useLegacyStore(ConfigStore); - const preloadData = shouldPreloadData(config); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const messages = useSelector(ConfigStore.store, state => state.messages); + const preloadData = useSelector(ConfigStore.store, shouldPreloadData); useEffect(() => { displayDeployPreviewAlert(addAlert); @@ -50,7 +51,7 @@ function AppAlerts() { }, [addAlert]); useEffect(() => { - if (!preloadData || !config.isSelfHosted) { + if (!preloadData || !isSelfHosted) { return; } api @@ -65,14 +66,14 @@ function AppAlerts() { .catch(() => { // TODO: do something? }); - }, [api, config.isSelfHosted, preloadData, addAlert]); + }, [api, isSelfHosted, preloadData, addAlert]); useEffect(() => { if (!preloadData) { return; } // Show system-level alerts that were forwarded by the initial client config request - config.messages.forEach(msg => + messages.forEach(msg => addAlert({ message: msg.message, variant: @@ -82,7 +83,7 @@ function AppAlerts() { neverExpire: true, }) ); - }, [config.messages, preloadData, addAlert]); + }, [messages, preloadData, addAlert]); return null; } @@ -165,8 +166,18 @@ function AppBody({ export function App() { const api = useApi(); const user = useUser(); - const config = useLegacyStore(ConfigStore); - const preloadData = shouldPreloadData(config); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const needsUpgrade = useSelector(ConfigStore.store, state => state.needsUpgrade); + const partnershipAgreementPrompt = useSelector( + ConfigStore.store, + state => state.partnershipAgreementPrompt + ); + const shouldShowBeaconConsentPrompt = useSelector( + ConfigStore.store, + state => state.shouldShowBeaconConsentPrompt + ); + const customerDomain = useSelector(ConfigStore.store, state => state.customerDomain); + const preloadData = useSelector(ConfigStore.store, shouldPreloadData); /** * Loads the users organization list into the OrganizationsStore @@ -229,12 +240,10 @@ export function App() { ConfigStore.set('user', {...user, flags}); } - const displayInstallWizard = - user?.isSuperuser && config.needsUpgrade && config.isSelfHosted; + const displayInstallWizard = user?.isSuperuser && needsUpgrade && isSelfHosted; const newsletterConsentPrompt = user?.flags?.newsletter_consent_prompt; - const partnershipAgreementPrompt = config.partnershipAgreementPrompt; const beaconConsentPrompt = - user?.isSuperuser && config.isSelfHosted && config.shouldShowBeaconConsentPrompt; + user?.isSuperuser && isSelfHosted && shouldShowBeaconConsentPrompt; return ( @@ -252,7 +261,7 @@ export function App() { displayInstallWizard={displayInstallWizard} isOrgSlugValid={isOrgSlugValid} newsletterConsentPrompt={newsletterConsentPrompt} - organizationSlug={config.customerDomain?.subdomain} + organizationSlug={customerDomain?.subdomain} partnershipAgreementPrompt={partnershipAgreementPrompt} /> diff --git a/static/app/views/app/root.tsx b/static/app/views/app/root.tsx index 44e8d950bf11..4bf7ae316d6f 100644 --- a/static/app/views/app/root.tsx +++ b/static/app/views/app/root.tsx @@ -1,8 +1,8 @@ import {useEffect} from 'react'; +import {useSelector} from '@tanstack/react-store'; import {DEFAULT_APP_ROUTE} from 'sentry/constants'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {replaceRouterParams} from 'sentry/utils/replaceRouterParams'; import {useNavigate} from 'sentry/utils/useNavigate'; @@ -18,7 +18,10 @@ import {useNavigate} from 'sentry/utils/useNavigate'; * in which case we should load their list of organizations and make a decision */ function AppRoot() { - const {lastOrganization} = useLegacyStore(ConfigStore); + const lastOrganization = useSelector( + ConfigStore.store, + state => state.lastOrganization + ); const navigate = useNavigate(); useEffect(() => { diff --git a/static/app/views/authV2/authLogin/hooks/useSingleOrganizationLogin.tsx b/static/app/views/authV2/authLogin/hooks/useSingleOrganizationLogin.tsx index f6fe02918075..ee8d71698796 100644 --- a/static/app/views/authV2/authLogin/hooks/useSingleOrganizationLogin.tsx +++ b/static/app/views/authV2/authLogin/hooks/useSingleOrganizationLogin.tsx @@ -1,7 +1,7 @@ import {useEffect} from 'react'; +import {useSelector} from '@tanstack/react-store'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {useNavigate} from 'sentry/utils/useNavigate'; interface UseSingleOrganizationLoginOptions { @@ -14,7 +14,10 @@ export function useSingleOrganizationLogin({ singleOrganizationSlug, }: UseSingleOrganizationLoginOptions) { const navigate = useNavigate(); - const isSingleOrganization = useLegacyStore(ConfigStore).singleOrganization; + const isSingleOrganization = useSelector( + ConfigStore.store, + state => state.singleOrganization + ); useEffect(() => { if (!isSingleOrganization || !singleOrganizationSlug) { diff --git a/static/app/views/authV2/authRegister/index.tsx b/static/app/views/authV2/authRegister/index.tsx index 07f0d363b834..815eb0393719 100644 --- a/static/app/views/authV2/authRegister/index.tsx +++ b/static/app/views/authV2/authRegister/index.tsx @@ -1,4 +1,5 @@ import {Fragment, useEffect} from 'react'; +import {useSelector} from '@tanstack/react-store'; import {Alert} from '@sentry/scraps/alert'; import {Flex, Stack} from '@sentry/scraps/layout'; @@ -7,7 +8,6 @@ import {Heading, Text} from '@sentry/scraps/text'; import {t, tct} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {testableWindowLocation} from 'sentry/utils/testableWindowLocation'; import {AuthCritter} from 'sentry/views/authV2/authCritter'; import {useAuthConfig} from 'sentry/views/authV2/authLogin/hooks/useAuthConfig'; @@ -16,7 +16,8 @@ import {useAuthPageLoading} from 'sentry/views/authV2/useAuthPageLoading'; import {RegistrationForm} from './registrationForm'; export default function AuthRegister() { - const {termsUrl, privacyUrl} = useLegacyStore(ConfigStore); + const termsUrl = useSelector(ConfigStore.store, state => state.termsUrl); + const privacyUrl = useSelector(ConfigStore.store, state => state.privacyUrl); const { data: authConfig, isPending: isAuthConfigPending, diff --git a/static/app/views/dashboards/adjustedFiltersAlert.spec.tsx b/static/app/views/dashboards/adjustedFiltersAlert.spec.tsx index f7bc3f76ecdb..e5fb7038407a 100644 --- a/static/app/views/dashboards/adjustedFiltersAlert.spec.tsx +++ b/static/app/views/dashboards/adjustedFiltersAlert.spec.tsx @@ -7,7 +7,7 @@ import {PageFiltersStore} from 'sentry/components/pageFilters/store'; import {AdjustedFiltersAlert} from 'sentry/views/dashboards/adjustedFiltersAlert'; describe('AdjustedFiltersAlert', () => { - afterEach(() => { + beforeEach(() => { PageFiltersStore.reset(); }); diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpOverview.spec.tsx b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpOverview.spec.tsx index acf59fdece20..0d79a7141171 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpOverview.spec.tsx +++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpOverview.spec.tsx @@ -67,6 +67,7 @@ function mockTransportRequests() { describe('MCP Transport Distribution', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState(selection); MockApiClient.addMockResponse({ url: '/organizations/org-slug/releases/stats/', @@ -74,10 +75,6 @@ describe('MCP Transport Distribution', () => { }); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - it('counts standard transports together and preserves custom names in the existing chart', async () => { const requests = mockTransportRequests(); diff --git a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.spec.tsx b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.spec.tsx index da7aa0efe654..2f0948c3e6a3 100644 --- a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.spec.tsx @@ -31,6 +31,7 @@ describe('NewWidgetBuilder', () => { const onSaveMock = jest.fn(); beforeEach(() => { + PageFiltersStore.reset(); OrganizationStore.init(); PageFiltersStore.init(); @@ -94,8 +95,6 @@ describe('NewWidgetBuilder', () => { }); }); - afterEach(() => PageFiltersStore.reset()); - it('renders', async () => { render( diff --git a/static/app/views/dashboards/widgets/detailsWidget/detailsWidgetVisualization.spec.tsx b/static/app/views/dashboards/widgets/detailsWidget/detailsWidgetVisualization.spec.tsx index 056cb2855e42..94ed50b81c8b 100644 --- a/static/app/views/dashboards/widgets/detailsWidget/detailsWidgetVisualization.spec.tsx +++ b/static/app/views/dashboards/widgets/detailsWidget/detailsWidgetVisualization.spec.tsx @@ -16,6 +16,7 @@ type TestSpan = Pick; describe('DetailsWidgetVisualization', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); const organization = OrganizationFixture(); const project = ProjectFixture(); @@ -54,7 +55,6 @@ describe('DetailsWidgetVisualization', () => { }); afterEach(() => { - PageFiltersStore.reset(); MockApiClient.clearMockResponses(); }); diff --git a/static/app/views/detectors/components/details/cron/index.spec.tsx b/static/app/views/detectors/components/details/cron/index.spec.tsx index 5a3eedcd4afc..79e36c32b6e6 100644 --- a/static/app/views/detectors/components/details/cron/index.spec.tsx +++ b/static/app/views/detectors/components/details/cron/index.spec.tsx @@ -47,6 +47,8 @@ describe('CronDetectorDetails - check-ins', () => { }); beforeEach(() => { + PageFiltersStore.reset(); + ProjectsStore.reset(); MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ @@ -166,8 +168,6 @@ describe('CronDetectorDetails - check-ins', () => { afterEach(() => { localStorageWrapper.removeItem('global-selection:org-slug'); - PageFiltersStore.reset(); - ProjectsStore.reset(); jest.restoreAllMocks(); }); diff --git a/static/app/views/discover/results.spec.tsx b/static/app/views/discover/results.spec.tsx index b816332ce2e0..c4956c57d03a 100644 --- a/static/app/views/discover/results.spec.tsx +++ b/static/app/views/discover/results.spec.tsx @@ -226,9 +226,12 @@ function renderMockRequests() { } describe('Results', () => { + beforeEach(() => { + ProjectsStore.reset(); + }); + afterEach(() => { MockApiClient.clearMockResponses(); - ProjectsStore.reset(); }); describe('Events', () => { const features = ['discover-basic']; diff --git a/static/app/views/discover/results/resultsChart.spec.tsx b/static/app/views/discover/results/resultsChart.spec.tsx index d7fdf531e010..23cb871a3b8f 100644 --- a/static/app/views/discover/results/resultsChart.spec.tsx +++ b/static/app/views/discover/results/resultsChart.spec.tsx @@ -169,11 +169,8 @@ describe('Discover > ResultsChart', () => { }); beforeEach(() => { - PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); - }); - - afterEach(() => { PageFiltersStore.reset(); + PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); }); function mockDroppedData() { diff --git a/static/app/views/discover/table/tableView.spec.tsx b/static/app/views/discover/table/tableView.spec.tsx index 7971208391d7..ce4f36e42df8 100644 --- a/static/app/views/discover/table/tableView.spec.tsx +++ b/static/app/views/discover/table/tableView.spec.tsx @@ -99,6 +99,7 @@ describe('TableView > CellActions', () => { } beforeEach(() => { + ProjectsStore.reset(); act(() => { ProjectsStore.loadInitialData(projects); TagStore.reset(); @@ -141,10 +142,6 @@ describe('TableView > CellActions', () => { }); }); - afterEach(() => { - ProjectsStore.reset(); - }); - it('updates sort order on equation fields', () => { const view = eventView.clone(); render(, renderOptions); diff --git a/static/app/views/explore/conversations/components/agentsTable.spec.tsx b/static/app/views/explore/conversations/components/agentsTable.spec.tsx index 1c6b17cb881e..42c681464bd1 100644 --- a/static/app/views/explore/conversations/components/agentsTable.spec.tsx +++ b/static/app/views/explore/conversations/components/agentsTable.spec.tsx @@ -26,6 +26,7 @@ const conversationsResult = { describe('AgentsTable', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.init(); PageFiltersStore.onInitializeUrlState({ projects: [Number(project.id)], @@ -52,7 +53,6 @@ describe('AgentsTable', () => { }); afterEach(() => { - PageFiltersStore.reset(); ProjectsStore.reset(); MockApiClient.clearMockResponses(); }); diff --git a/static/app/views/explore/conversations/hooks/useShowConversationOnboarding.spec.tsx b/static/app/views/explore/conversations/hooks/useShowConversationOnboarding.spec.tsx index eb456b39d79c..0e82c2fc1826 100644 --- a/static/app/views/explore/conversations/hooks/useShowConversationOnboarding.spec.tsx +++ b/static/app/views/explore/conversations/hooks/useShowConversationOnboarding.spec.tsx @@ -45,6 +45,8 @@ function knownConversations(projects: number[]) { describe('useShowConversationOnboarding', () => { beforeEach(() => { + PageFiltersStore.reset(); + ProjectsStore.reset(); localStorage.clear(); ProjectsStore.loadInitialData([ ProjectFixture({id: '1', hasInsightsAgentMonitoring: false}), @@ -57,8 +59,6 @@ describe('useShowConversationOnboarding', () => { afterEach(() => { localStorage.clear(); - PageFiltersStore.reset(); - ProjectsStore.reset(); MockApiClient.clearMockResponses(); }); diff --git a/static/app/views/explore/conversations/onboarding.spec.tsx b/static/app/views/explore/conversations/onboarding.spec.tsx index 2343dcee057e..82c3e891a0f1 100644 --- a/static/app/views/explore/conversations/onboarding.spec.tsx +++ b/static/app/views/explore/conversations/onboarding.spec.tsx @@ -20,6 +20,7 @@ jest.mock('sentry/utils/analytics'); describe('ConversationOnboarding', () => { beforeEach(() => { + ProjectsStore.reset(); Object.assign(navigator, { clipboard: {writeText: jest.fn().mockResolvedValue(undefined)}, }); @@ -57,7 +58,6 @@ describe('ConversationOnboarding', () => { } afterEach(() => { - ProjectsStore.reset(); MockApiClient.clearMockResponses(); jest.clearAllMocks(); }); diff --git a/static/app/views/explore/conversations/onboarding.tsx b/static/app/views/explore/conversations/onboarding.tsx index 047bbdc1578c..f2c33d276626 100644 --- a/static/app/views/explore/conversations/onboarding.tsx +++ b/static/app/views/explore/conversations/onboarding.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import {IconBot} from '@sentry/icons/bot'; import {IconCopy} from '@sentry/icons/copy'; import {IconUser} from '@sentry/icons/user'; +import {useSelector} from '@tanstack/react-store'; import agentTracingEmptyStateImg from 'sentry-images/spot/agent-tracing-empty-state.svg'; @@ -44,7 +45,6 @@ import {agentMonitoringPlatforms} from 'sentry/data/platformCategories'; import {otherPlatform, allPlatforms as platforms} from 'sentry/data/platforms'; import {t, tct} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; import {decodeInteger} from 'sentry/utils/queryString'; @@ -532,7 +532,8 @@ const INTEGRATIONS_WITH_AUTOMATIC_CONVERSATION_IDS = new Set([ export function ConversationOnboarding({onDismiss}: {onDismiss: () => void}) { const api = useApi(); - const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const urlPrefix = useSelector(ConfigStore.store, state => state.urlPrefix); const project = useOnboardingProject(); const organization = useOrganization(); const location = useLocation(); diff --git a/static/app/views/explore/conversations/overview.spec.tsx b/static/app/views/explore/conversations/overview.spec.tsx index 0d766259f3ce..f15bcf2f882e 100644 --- a/static/app/views/explore/conversations/overview.spec.tsx +++ b/static/app/views/explore/conversations/overview.spec.tsx @@ -24,6 +24,7 @@ const LLM_QUERY = 'gen_ai.operation.type:ai_client'; describe('ConversationsOverviewPage', () => { beforeEach(() => { + ProjectsStore.reset(); PageFiltersStore.init(); ProjectsStore.loadInitialData([ ProjectFixture({id: '1', hasInsightsAgentMonitoring: true}), @@ -109,7 +110,6 @@ describe('ConversationsOverviewPage', () => { afterEach(() => { localStorage.clear(); - ProjectsStore.reset(); MockApiClient.clearMockResponses(); }); diff --git a/static/app/views/explore/hooks/useAttributeBreakdowns.spec.tsx b/static/app/views/explore/hooks/useAttributeBreakdowns.spec.tsx index a1c1b8284cb1..cfdab16cf5ee 100644 --- a/static/app/views/explore/hooks/useAttributeBreakdowns.spec.tsx +++ b/static/app/views/explore/hooks/useAttributeBreakdowns.spec.tsx @@ -8,15 +8,12 @@ import {useAttributeBreakdowns} from 'sentry/views/explore/hooks/useAttributeBre describe('useAttributeBreakdowns', () => { beforeEach(() => { + PageFiltersStore.reset(); MockApiClient.clearMockResponses(); PageFiltersStore.init(); PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - it('sends the case setting when ignoring case', async () => { const statsMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/trace-items/stats/', diff --git a/static/app/views/explore/hooks/useExploreAggregatesTable.spec.tsx b/static/app/views/explore/hooks/useExploreAggregatesTable.spec.tsx index 058bc83bff47..a330c7ea88dd 100644 --- a/static/app/views/explore/hooks/useExploreAggregatesTable.spec.tsx +++ b/static/app/views/explore/hooks/useExploreAggregatesTable.spec.tsx @@ -14,14 +14,11 @@ function Wrapper({children}: {children: ReactNode}) { } describe('useExploreAggregatesTable', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); jest.clearAllMocks(); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - it('triggers the high accuracy request when there is no data and a partial scan', async () => { const mockNormalRequestUrl = MockApiClient.addMockResponse({ url: '/organizations/org-slug/events/', diff --git a/static/app/views/explore/hooks/useExploreSpansTable.spec.tsx b/static/app/views/explore/hooks/useExploreSpansTable.spec.tsx index ff7cc070b213..391c3159ed83 100644 --- a/static/app/views/explore/hooks/useExploreSpansTable.spec.tsx +++ b/static/app/views/explore/hooks/useExploreSpansTable.spec.tsx @@ -30,14 +30,11 @@ function useTestExploreSpansTable(query: string) { describe('useExploreSpansTable', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); jest.clearAllMocks(); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - it('triggers the high accuracy request when there is no data and a partial scan', async () => { const mockNormalRequestUrl = MockApiClient.addMockResponse({ url: '/organizations/org-slug/events/', diff --git a/static/app/views/explore/hooks/useExploreTimeseries.spec.tsx b/static/app/views/explore/hooks/useExploreTimeseries.spec.tsx index f8361e732978..4789edd50729 100644 --- a/static/app/views/explore/hooks/useExploreTimeseries.spec.tsx +++ b/static/app/views/explore/hooks/useExploreTimeseries.spec.tsx @@ -20,14 +20,11 @@ function Wrapper({children}: {children: ReactNode}) { describe('useExploreTimeseries', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); jest.clearAllMocks(); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - it('keeps transformations aligned after sorting and deduplicating axes', async () => { const request = MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-timeseries/', diff --git a/static/app/views/explore/hooks/useSaveQuery.spec.tsx b/static/app/views/explore/hooks/useSaveQuery.spec.tsx index d5e5abf97e42..9a4184015f67 100644 --- a/static/app/views/explore/hooks/useSaveQuery.spec.tsx +++ b/static/app/views/explore/hooks/useSaveQuery.spec.tsx @@ -16,6 +16,7 @@ describe('useSpansSaveQuery', () => { const organization = OrganizationFixture(); beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState( PageFiltersFixture({ projects: [1], @@ -31,7 +32,6 @@ describe('useSpansSaveQuery', () => { }); afterEach(() => { - PageFiltersStore.reset(); MockApiClient.clearMockResponses(); }); diff --git a/static/app/views/explore/logs/logsOnboarding.tsx b/static/app/views/explore/logs/logsOnboarding.tsx index f5bc1db28535..5670e21ac067 100644 --- a/static/app/views/explore/logs/logsOnboarding.tsx +++ b/static/app/views/explore/logs/logsOnboarding.tsx @@ -1,6 +1,7 @@ import {Fragment, useEffect} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import connectDotsImg from 'sentry-images/spot/performance-connect-dots.svg'; @@ -38,7 +39,6 @@ import {withoutLoggingSupport} from 'sentry/data/platformCategories'; import {otherPlatform, allPlatforms as platforms} from 'sentry/data/platforms'; import {t, tct} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {pulsingIndicatorStyles} from 'sentry/styles/pulsingIndicator'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; @@ -261,7 +261,8 @@ function Onboarding({organization, project}: OnboardingProps) { const api = useApi(); const location = useLocation(); const navigate = useNavigate(); - const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const urlPrefix = useSelector(ConfigStore.store, state => state.urlPrefix); const currentPlatform = project.platform ? platforms.find(p => p.id === project.platform) : undefined; diff --git a/static/app/views/explore/logs/useLogsQuery.spec.tsx b/static/app/views/explore/logs/useLogsQuery.spec.tsx index 6d557ccb9403..bb042eb7f03a 100644 --- a/static/app/views/explore/logs/useLogsQuery.spec.tsx +++ b/static/app/views/explore/logs/useLogsQuery.spec.tsx @@ -56,6 +56,7 @@ describe('useInfiniteLogsQuery', () => { let mockNow: jest.SpyInstance; beforeEach(() => { + PageFiltersStore.reset(); jest.resetAllMocks(); mockNow = jest.spyOn(Date, 'now'); MockApiClient.clearMockResponses(); @@ -66,7 +67,6 @@ describe('useInfiniteLogsQuery', () => { afterEach(() => { mockNow.mockRestore(); - PageFiltersStore.reset(); }); it('should not fetch logs when disabled', () => { @@ -1056,6 +1056,7 @@ describe('Virtual Streaming Integration (Auto Refresh Behaviour)', () => { let mockNowInner: jest.SpyInstance; beforeEach(() => { + PageFiltersStore.reset(); jest.resetAllMocks(); // jest.resetAllMocks() clears any outer Date.now spy, so we need to re-mock it here // with a realistic timestamp so that initializeVirtualTimestamp computes a sane @@ -1069,7 +1070,6 @@ describe('Virtual Streaming Integration (Auto Refresh Behaviour)', () => { afterEach(() => { mockNowInner.mockRestore(); - PageFiltersStore.reset(); }); it('should integrate with virtual streaming when auto refresh is enabled', async () => { diff --git a/static/app/views/explore/logs/useLogsSearchQueryBuilderProps.spec.tsx b/static/app/views/explore/logs/useLogsSearchQueryBuilderProps.spec.tsx index 22b720e708b1..951aebd9eb1d 100644 --- a/static/app/views/explore/logs/useLogsSearchQueryBuilderProps.spec.tsx +++ b/static/app/views/explore/logs/useLogsSearchQueryBuilderProps.spec.tsx @@ -45,6 +45,7 @@ const arrayFeatures = {features: ['trace-item-array-query-support']}; describe('useLogsSearchQueryBuilderProps', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState( PageFiltersFixture({ datetime: {period: '14d', start: null, end: null, utc: false}, @@ -55,7 +56,6 @@ describe('useLogsSearchQueryBuilderProps', () => { }); afterEach(() => { - PageFiltersStore.reset(); jest.clearAllMocks(); }); diff --git a/static/app/views/explore/logs/useLogsTimeseriesRequest.spec.tsx b/static/app/views/explore/logs/useLogsTimeseriesRequest.spec.tsx index 8546cc3bf514..13b96613fb38 100644 --- a/static/app/views/explore/logs/useLogsTimeseriesRequest.spec.tsx +++ b/static/app/views/explore/logs/useLogsTimeseriesRequest.spec.tsx @@ -28,11 +28,8 @@ const aggregateFields = [ describe('useLogsTimeseriesRequest', () => { beforeEach(() => { - PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); - }); - - afterEach(() => { PageFiltersStore.reset(); + PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); }); it.each([false, true])( diff --git a/static/app/views/explore/logs/useSaveAsItems.spec.tsx b/static/app/views/explore/logs/useSaveAsItems.spec.tsx index cd4f6e3ccb0b..1cca541da196 100644 --- a/static/app/views/explore/logs/useSaveAsItems.spec.tsx +++ b/static/app/views/explore/logs/useSaveAsItems.spec.tsx @@ -52,6 +52,8 @@ describe('useSaveAsItems', () => { } beforeEach(() => { + PageFiltersStore.reset(); + ProjectsStore.reset(); jest.resetAllMocks(); MockApiClient.clearMockResponses(); ProjectsStore.loadInitialData([project]); @@ -76,11 +78,6 @@ describe('useSaveAsItems', () => { }); }); - afterEach(() => { - PageFiltersStore.reset(); - ProjectsStore.reset(); - }); - it('should open save query modal when save as new query is clicked', () => { const {result} = renderHookWithProviders(useSaveAsItems, { additionalWrapper: Wrapper, diff --git a/static/app/views/explore/logs/useValidateLogsTab.spec.tsx b/static/app/views/explore/logs/useValidateLogsTab.spec.tsx index f1d36aaf8226..b460059c55b6 100644 --- a/static/app/views/explore/logs/useValidateLogsTab.spec.tsx +++ b/static/app/views/explore/logs/useValidateLogsTab.spec.tsx @@ -42,6 +42,7 @@ const validationBody: EventValidationData = { describe('useValidateLogsTab', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState( PageFiltersFixture({ datetime: {period: '14d', start: null, end: null, utc: false}, @@ -52,7 +53,6 @@ describe('useValidateLogsTab', () => { }); afterEach(() => { - PageFiltersStore.reset(); MockApiClient.clearMockResponses(); jest.clearAllMocks(); }); diff --git a/static/app/views/explore/metrics/exports/metricsAggregateExportModalButton.spec.tsx b/static/app/views/explore/metrics/exports/metricsAggregateExportModalButton.spec.tsx index 44892a48fc6d..b4718d0fdfa4 100644 --- a/static/app/views/explore/metrics/exports/metricsAggregateExportModalButton.spec.tsx +++ b/static/app/views/explore/metrics/exports/metricsAggregateExportModalButton.spec.tsx @@ -98,6 +98,7 @@ describe('MetricsAggregateExportModalButton', () => { } beforeEach(() => { + PageFiltersStore.reset(); MockApiClient.clearMockResponses(); jest.clearAllMocks(); PageFiltersStore.onInitializeUrlState( @@ -107,10 +108,6 @@ describe('MetricsAggregateExportModalButton', () => { ); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - it('downloads locally without a server export when all rows are loaded', async () => { const exportRequest = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/data-export/`, diff --git a/static/app/views/explore/metrics/exports/metricsSamplesExportModalButton.spec.tsx b/static/app/views/explore/metrics/exports/metricsSamplesExportModalButton.spec.tsx index 388740f49b93..6da14cc4e49d 100644 --- a/static/app/views/explore/metrics/exports/metricsSamplesExportModalButton.spec.tsx +++ b/static/app/views/explore/metrics/exports/metricsSamplesExportModalButton.spec.tsx @@ -114,6 +114,7 @@ describe('MetricsSamplesExportModalButton', () => { let eventsRequest: jest.Mock; beforeEach(() => { + PageFiltersStore.reset(); MockApiClient.clearMockResponses(); jest.clearAllMocks(); PageFiltersStore.onInitializeUrlState( @@ -128,10 +129,6 @@ describe('MetricsSamplesExportModalButton', () => { }); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - it('asks the server export for the accuracy the table escalates to', async () => { const queryInfo = await submitExport(); diff --git a/static/app/views/explore/metrics/hooks/useMetricAggregatesTable.spec.tsx b/static/app/views/explore/metrics/hooks/useMetricAggregatesTable.spec.tsx index 9810e26d36bd..ed4187f4641e 100644 --- a/static/app/views/explore/metrics/hooks/useMetricAggregatesTable.spec.tsx +++ b/static/app/views/explore/metrics/hooks/useMetricAggregatesTable.spec.tsx @@ -13,14 +13,11 @@ import {VisualizeFunction} from 'sentry/views/explore/queryParams/visualize'; describe('useMetricAggregatesTable', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); jest.clearAllMocks(); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - it('triggers the high accuracy request when there is no data and a partial scan', async () => { const mockNormalRequestUrl = MockApiClient.addMockResponse({ url: '/organizations/org-slug/events/', diff --git a/static/app/views/explore/metrics/hooks/useMetricSamplesTable.spec.tsx b/static/app/views/explore/metrics/hooks/useMetricSamplesTable.spec.tsx index b9c0e74f5404..aeddd1674afc 100644 --- a/static/app/views/explore/metrics/hooks/useMetricSamplesTable.spec.tsx +++ b/static/app/views/explore/metrics/hooks/useMetricSamplesTable.spec.tsx @@ -9,6 +9,7 @@ import {useMetricSamplesTable} from 'sentry/views/explore/metrics/hooks/useMetri describe('useMetricSamplesTable', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState( PageFiltersFixture({ projects: [1, 2], @@ -24,10 +25,6 @@ describe('useMetricSamplesTable', () => { jest.clearAllMocks(); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - it('triggers the high accuracy request when there is no data and a partial scan', async () => { const mockNormalRequestUrl = MockApiClient.addMockResponse({ url: '/organizations/org-slug/events/', diff --git a/static/app/views/explore/metrics/hooks/useMetricTimeseries.spec.tsx b/static/app/views/explore/metrics/hooks/useMetricTimeseries.spec.tsx index 56b8f6c7e7c2..593370fc0e2b 100644 --- a/static/app/views/explore/metrics/hooks/useMetricTimeseries.spec.tsx +++ b/static/app/views/explore/metrics/hooks/useMetricTimeseries.spec.tsx @@ -59,14 +59,11 @@ function MockMetricQueryParamsContextWithMultiVisualize({ describe('useMetricTimeseries', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); jest.clearAllMocks(); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - it.each([true, false])( 'forwards metric transformations with the feature enabled=%s', async featureEnabled => { diff --git a/static/app/views/explore/metrics/hooks/useSaveMetricsMultiQuery.spec.tsx b/static/app/views/explore/metrics/hooks/useSaveMetricsMultiQuery.spec.tsx index 8d715020d387..8834fb84fb14 100644 --- a/static/app/views/explore/metrics/hooks/useSaveMetricsMultiQuery.spec.tsx +++ b/static/app/views/explore/metrics/hooks/useSaveMetricsMultiQuery.spec.tsx @@ -79,12 +79,12 @@ function TestPage() { describe('useSaveMetricsMultiQuery', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.init(); PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); }); afterEach(() => { - PageFiltersStore.reset(); MockApiClient.clearMockResponses(); }); diff --git a/static/app/views/explore/metrics/hooks/useValidateMetricsTab.spec.tsx b/static/app/views/explore/metrics/hooks/useValidateMetricsTab.spec.tsx index f9d1e37d04eb..b745a3508927 100644 --- a/static/app/views/explore/metrics/hooks/useValidateMetricsTab.spec.tsx +++ b/static/app/views/explore/metrics/hooks/useValidateMetricsTab.spec.tsx @@ -28,6 +28,7 @@ const validationBody: EventValidationData = { describe('useValidateMetricsTab', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState( PageFiltersFixture({ datetime: {period: '14d', start: null, end: null, utc: false}, @@ -38,7 +39,6 @@ describe('useValidateMetricsTab', () => { }); afterEach(() => { - PageFiltersStore.reset(); MockApiClient.clearMockResponses(); jest.clearAllMocks(); }); diff --git a/static/app/views/explore/metrics/metricsOnboarding.tsx b/static/app/views/explore/metrics/metricsOnboarding.tsx index e7396fc3d414..71840a03dabb 100644 --- a/static/app/views/explore/metrics/metricsOnboarding.tsx +++ b/static/app/views/explore/metrics/metricsOnboarding.tsx @@ -1,6 +1,7 @@ import {useEffect} from 'react'; import {useTheme, css} from '@emotion/react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import connectDotsImg from 'sentry-images/spot/performance-connect-dots.svg'; @@ -35,7 +36,6 @@ import {withoutMetricsSupport} from 'sentry/data/platformCategories'; import {otherPlatform, allPlatforms as platforms} from 'sentry/data/platforms'; import {t, tct} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -117,7 +117,8 @@ function Onboarding({organization, project}: OnboardingProps) { const api = useApi(); const location = useLocation(); const navigate = useNavigate(); - const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const urlPrefix = useSelector(ConfigStore.store, state => state.urlPrefix); const currentPlatform = project.platform ? platforms.find(p => p.id === project.platform) : undefined; diff --git a/static/app/views/explore/metrics/useSaveAsMetricItems.spec.tsx b/static/app/views/explore/metrics/useSaveAsMetricItems.spec.tsx index 8e728340eaeb..c5cd135d26b8 100644 --- a/static/app/views/explore/metrics/useSaveAsMetricItems.spec.tsx +++ b/static/app/views/explore/metrics/useSaveAsMetricItems.spec.tsx @@ -45,6 +45,7 @@ describe('useSaveAsMetricItems', () => { } beforeEach(() => { + ProjectsStore.reset(); jest.resetAllMocks(); MockApiClient.clearMockResponses(); ProjectsStore.loadInitialData([project]); @@ -56,10 +57,6 @@ describe('useSaveAsMetricItems', () => { }); }); - afterEach(() => { - ProjectsStore.reset(); - }); - it('should open save query modal when save as new query is clicked', () => { const {result} = renderHookWithProviders(useSaveAsMetricItems, { organization, diff --git a/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTimeseries.spec.tsx b/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTimeseries.spec.tsx index d5fd94c9604d..4be734ed4dcc 100644 --- a/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTimeseries.spec.tsx +++ b/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTimeseries.spec.tsx @@ -15,14 +15,11 @@ describe('useMultiQueryTimeseries', () => { let mockNormalRequestUrl: jest.Mock; beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); jest.clearAllMocks(); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - it('triggers the high accuracy request when there is no data and a partial scan', async () => { jest.mocked(useReadQueriesFromLocation).mockReturnValue([ { diff --git a/static/app/views/explore/profiling/onboarding.tsx b/static/app/views/explore/profiling/onboarding.tsx index 0911e87cc0b1..e1e5e24f2904 100644 --- a/static/app/views/explore/profiling/onboarding.tsx +++ b/static/app/views/explore/profiling/onboarding.tsx @@ -1,5 +1,6 @@ import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import emptyTraceImg from 'sentry-images/spot/profiling-empty-state.svg'; @@ -33,7 +34,6 @@ import {profiling as profilingPlatforms} from 'sentry/data/platformCategories'; import {otherPlatform, allPlatforms as platforms} from 'sentry/data/platforms'; import {t, tct} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {pulsingIndicatorStyles} from 'sentry/styles/pulsingIndicator'; import type {Project} from 'sentry/types/project'; import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents'; @@ -200,7 +200,8 @@ function OnboardingPanel({ export function Onboarding() { const api = useApi(); - const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const urlPrefix = useSelector(ConfigStore.store, state => state.urlPrefix); const project = useOnboardingProject(); const organization = useOrganization(); diff --git a/static/app/views/explore/releases/list/index.spec.tsx b/static/app/views/explore/releases/list/index.spec.tsx index 1bc4c24a9b22..75cd819e30a1 100644 --- a/static/app/views/explore/releases/list/index.spec.tsx +++ b/static/app/views/explore/releases/list/index.spec.tsx @@ -44,6 +44,7 @@ describe('ReleasesList', () => { let sdkVersionsMock: jest.Mock; beforeEach(() => { + ProjectsStore.reset(); act(() => ProjectsStore.loadInitialData(projects)); act(() => PageFiltersStore.onInitializeUrlState({ @@ -108,7 +109,6 @@ describe('ReleasesList', () => { }); afterEach(() => { - act(() => ProjectsStore.reset()); MockApiClient.clearMockResponses(); }); diff --git a/static/app/views/explore/replays/detail/header/replayDetailsEntityHeader.spec.tsx b/static/app/views/explore/replays/detail/header/replayDetailsEntityHeader.spec.tsx index 1349af722178..240ca58ffc18 100644 --- a/static/app/views/explore/replays/detail/header/replayDetailsEntityHeader.spec.tsx +++ b/static/app/views/explore/replays/detail/header/replayDetailsEntityHeader.spec.tsx @@ -60,6 +60,7 @@ function readerResultFixture( jest.useFakeTimers(); describe('ReplayDetailsEntityHeader', () => { beforeEach(() => { + ProjectsStore.reset(); MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/projects/`, @@ -68,10 +69,6 @@ describe('ReplayDetailsEntityHeader', () => { ProjectsStore.loadInitialData([project]); }); - afterEach(() => { - ProjectsStore.reset(); - }); - function mockViewedBy(replayId: string) { return MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/${project.slug}/replays/${replayId}/viewed-by/`, diff --git a/static/app/views/explore/spans/charts/index.spec.tsx b/static/app/views/explore/spans/charts/index.spec.tsx index b2b3d15f22dd..cbae9f1d0f05 100644 --- a/static/app/views/explore/spans/charts/index.spec.tsx +++ b/static/app/views/explore/spans/charts/index.spec.tsx @@ -415,11 +415,8 @@ describe('ExploreCharts', () => { const features = ['explore-data-fidelity-annotations']; beforeEach(() => { - PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); - }); - - afterEach(() => { PageFiltersStore.reset(); + PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); }); function renderCharts({ diff --git a/static/app/views/explore/spans/content.tsx b/static/app/views/explore/spans/content.tsx index c6b093ec5396..4823300584fb 100644 --- a/static/app/views/explore/spans/content.tsx +++ b/static/app/views/explore/spans/content.tsx @@ -2,6 +2,7 @@ import {Fragment, useEffect, useMemo} from 'react'; import type {ReactNode} from 'react'; import * as Sentry from '@sentry/react'; import {useQuery} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import {Stack} from '@sentry/scraps/layout'; @@ -17,7 +18,6 @@ import {TourContextProvider} from 'sentry/components/tours/components'; import {useAssistant} from 'sentry/components/tours/useAssistant'; import {t} from 'sentry/locale'; import {OrganizationStore} from 'sentry/stores/organizationStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {DataCategory} from 'sentry/types/core'; import {trackAnalytics} from 'sentry/utils/analytics'; import {defined} from 'sentry/utils/defined'; @@ -76,7 +76,10 @@ function ExploreContentInner() { }); const organization = useOrganization(); - const {loading: organizationLoading} = useLegacyStore(OrganizationStore); + const organizationLoading = useSelector( + OrganizationStore.store, + state => state.loading + ); const {data: bootstrapOrganization, isPending: isBootstrapOrganizationPending} = useQuery(getBootstrapOrganizationQueryOptions(organization.slug)); diff --git a/static/app/views/explore/spans/hooks/useValidateSpansTab.spec.tsx b/static/app/views/explore/spans/hooks/useValidateSpansTab.spec.tsx index e6d47fc2d400..a01d41410c1a 100644 --- a/static/app/views/explore/spans/hooks/useValidateSpansTab.spec.tsx +++ b/static/app/views/explore/spans/hooks/useValidateSpansTab.spec.tsx @@ -29,6 +29,7 @@ const validationBody: EventValidationData = { describe('useValidateSpansTab', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState( PageFiltersFixture({ datetime: {period: '14d', start: null, end: null, utc: false}, @@ -39,7 +40,6 @@ describe('useValidateSpansTab', () => { }); afterEach(() => { - PageFiltersStore.reset(); MockApiClient.clearMockResponses(); jest.clearAllMocks(); }); diff --git a/static/app/views/explore/tables/aggregatesTable.spec.tsx b/static/app/views/explore/tables/aggregatesTable.spec.tsx index d5e7dedda5a9..f729fe217d4d 100644 --- a/static/app/views/explore/tables/aggregatesTable.spec.tsx +++ b/static/app/views/explore/tables/aggregatesTable.spec.tsx @@ -118,6 +118,7 @@ describe('AggregatesTable', () => { }; beforeEach(() => { + ProjectsStore.reset(); MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/trace-items/attributes/`, @@ -142,10 +143,6 @@ describe('AggregatesTable', () => { }); }); - afterEach(() => { - ProjectsStore.reset(); - }); - it('shows the query timeout message instead of a generic loading error', () => { render( { const eventView = EventView.fromLocation(location); beforeEach(() => { + ProjectsStore.reset(); const mockTimestamp = new Date('2024-10-06T00:00:00').getTime(); setMockDate(mockTimestamp); @@ -67,7 +68,6 @@ describe('FieldRenderer tests', () => { afterEach(() => { jest.restoreAllMocks(); resetMockDate(); - ProjectsStore.reset(); }); it('renders span.op', () => { diff --git a/static/app/views/insights/common/components/fullSpanDescription.spec.tsx b/static/app/views/insights/common/components/fullSpanDescription.spec.tsx index b82c4c5c3839..0f85869d5f96 100644 --- a/static/app/views/insights/common/components/fullSpanDescription.spec.tsx +++ b/static/app/views/insights/common/components/fullSpanDescription.spec.tsx @@ -10,14 +10,11 @@ import {ModuleName} from 'sentry/views/insights/types'; describe('FullSpanDescription', () => { beforeEach(() => { + PageFiltersStore.reset(); jest.clearAllMocks(); PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - const organization = OrganizationFixture(); const project = ProjectFixture(); diff --git a/static/app/views/insights/common/components/modulePageProviders.spec.tsx b/static/app/views/insights/common/components/modulePageProviders.spec.tsx index ab974c21fce8..f1292aa9f1fd 100644 --- a/static/app/views/insights/common/components/modulePageProviders.spec.tsx +++ b/static/app/views/insights/common/components/modulePageProviders.spec.tsx @@ -13,12 +13,12 @@ jest.mock('sentry/views/insights/common/utils/useHasDataTrackAnalytics'); describe('ModulePageProviders', () => { beforeEach(() => { + PageFiltersStore.reset(); PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); }); afterEach(() => { jest.resetAllMocks(); - PageFiltersStore.reset(); }); it('renders without module feature', async () => { diff --git a/static/app/views/insights/common/components/spanDescription.spec.tsx b/static/app/views/insights/common/components/spanDescription.spec.tsx index c423518e3440..6bc9ea7bd036 100644 --- a/static/app/views/insights/common/components/spanDescription.spec.tsx +++ b/static/app/views/insights/common/components/spanDescription.spec.tsx @@ -10,14 +10,11 @@ import {DatabaseSpanDescription} from 'sentry/views/insights/common/components/s describe('DatabaseSpanDescription', () => { beforeEach(() => { + PageFiltersStore.reset(); jest.clearAllMocks(); PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - const organization = OrganizationFixture(); const project = ProjectFixture(); diff --git a/static/app/views/insights/common/queries/useSortedTimeSeries.spec.tsx b/static/app/views/insights/common/queries/useSortedTimeSeries.spec.tsx index 4de4cdc69b6b..0ffa7ffd8b40 100644 --- a/static/app/views/insights/common/queries/useSortedTimeSeries.spec.tsx +++ b/static/app/views/insights/common/queries/useSortedTimeSeries.spec.tsx @@ -10,11 +10,8 @@ import {useSortedTimeSeries} from 'sentry/views/insights/common/queries/useSorte describe('useSortedTimeSeries', () => { beforeEach(() => { - PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); - }); - - afterEach(() => { PageFiltersStore.reset(); + PageFiltersStore.onInitializeUrlState(PageFiltersFixture()); }); it('selects raw and smoothed values separately for each group', async () => { diff --git a/static/app/views/insights/common/utils/useDefaultToAllProjects.spec.tsx b/static/app/views/insights/common/utils/useDefaultToAllProjects.spec.tsx index ea6f8fe071f5..6f74da9c28df 100644 --- a/static/app/views/insights/common/utils/useDefaultToAllProjects.spec.tsx +++ b/static/app/views/insights/common/utils/useDefaultToAllProjects.spec.tsx @@ -22,13 +22,13 @@ const pageFilterSelection = PageFiltersFixture({ describe('useDefaultToAllProjects', () => { beforeEach(() => { + PageFiltersStore.reset(); + ProjectsStore.reset(); PageFiltersStore.onInitializeUrlState(pageFilterSelection); }); afterEach(() => { jest.clearAllMocks(); - PageFiltersStore.reset(); - ProjectsStore.reset(); }); it('should default to all projects when no projects are selected and user has no team projects', () => { const nonMemberProject = ProjectFixture({isMember: false}); diff --git a/static/app/views/insights/database/components/noDataMessage.spec.tsx b/static/app/views/insights/database/components/noDataMessage.spec.tsx index 7657b979aeb2..e05997f0a353 100644 --- a/static/app/views/insights/database/components/noDataMessage.spec.tsx +++ b/static/app/views/insights/database/components/noDataMessage.spec.tsx @@ -11,6 +11,7 @@ import {NoDataMessage} from 'sentry/views/insights/database/components/noDataMes describe('NoDataMessage', () => { beforeEach(() => { + PageFiltersStore.reset(); jest.clearAllMocks(); MockApiClient.clearMockResponses(); @@ -26,7 +27,6 @@ describe('NoDataMessage', () => { }); afterEach(() => { - PageFiltersStore.reset(); ProjectsStore.reset(); }); diff --git a/static/app/views/insights/mobile/common/queries/useCrossPlatformProject.spec.tsx b/static/app/views/insights/mobile/common/queries/useCrossPlatformProject.spec.tsx index 589cd7275119..ec8b4a913696 100644 --- a/static/app/views/insights/mobile/common/queries/useCrossPlatformProject.spec.tsx +++ b/static/app/views/insights/mobile/common/queries/useCrossPlatformProject.spec.tsx @@ -25,16 +25,13 @@ function initializePageFilters(projects: number[]) { describe('useCrossPlatformProject', () => { let mockProject: Project; beforeEach(() => { + PageFiltersStore.reset(); + ProjectsStore.reset(); mockProject = ProjectFixture({platform: 'flutter'}); PageFiltersStore.init(); ProjectsStore.loadInitialData([mockProject]); }); - afterEach(() => { - PageFiltersStore.reset(); - ProjectsStore.reset(); - }); - it('returns null for project if >1 project is selected', () => { initializePageFilters([1, 2, 3]); diff --git a/static/app/views/insights/pages/agents/onboarding.spec.tsx b/static/app/views/insights/pages/agents/onboarding.spec.tsx index 7aeefbbacc5b..55326ee67906 100644 --- a/static/app/views/insights/pages/agents/onboarding.spec.tsx +++ b/static/app/views/insights/pages/agents/onboarding.spec.tsx @@ -57,6 +57,10 @@ describe('UnsupportedPlatformOnboarding', () => { }); describe('Onboarding deployment target', () => { + beforeEach(() => { + ProjectsStore.reset(); + }); + function setupProject(platform: PlatformKey) { const organization = OrganizationFixture(); const project = ProjectFixture({platform, firstTransactionEvent: false}); @@ -89,7 +93,6 @@ describe('Onboarding deployment target', () => { } afterEach(() => { - ProjectsStore.reset(); MockApiClient.clearMockResponses(); }); diff --git a/static/app/views/insights/pages/agents/onboarding.tsx b/static/app/views/insights/pages/agents/onboarding.tsx index 69b1887aab35..25ab6be933b6 100644 --- a/static/app/views/insights/pages/agents/onboarding.tsx +++ b/static/app/views/insights/pages/agents/onboarding.tsx @@ -1,5 +1,6 @@ import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import agentTracingEmptyStateImg from 'sentry-images/spot/agent-tracing-empty-state.svg'; @@ -36,7 +37,6 @@ import {agentMonitoringPlatforms} from 'sentry/data/platformCategories'; import {otherPlatform, allPlatforms as platforms} from 'sentry/data/platforms'; import {t, tct} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; import {decodeInteger} from 'sentry/utils/queryString'; @@ -201,7 +201,8 @@ function OnboardingPanel({ export function Onboarding() { const api = useApi(); - const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const urlPrefix = useSelector(ConfigStore.store, state => state.urlPrefix); const project = useOnboardingProject(); const organization = useOrganization(); const location = useLocation(); diff --git a/static/app/views/insights/pages/mcp/onboarding.tsx b/static/app/views/insights/pages/mcp/onboarding.tsx index f8461856e7dd..a6d79a5d2794 100644 --- a/static/app/views/insights/pages/mcp/onboarding.tsx +++ b/static/app/views/insights/pages/mcp/onboarding.tsx @@ -1,5 +1,6 @@ import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import emptyTraceImg from 'sentry-images/spot/profiling-empty-state.svg'; @@ -34,7 +35,6 @@ import {mcpMonitoringPlatforms} from 'sentry/data/platformCategories'; import {otherPlatform, allPlatforms as platforms} from 'sentry/data/platforms'; import {t, tct} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {pulsingIndicatorStyles} from 'sentry/styles/pulsingIndicator'; import type {Project} from 'sentry/types/project'; import {getSelectedProjectList} from 'sentry/utils/project/useSelectedProjectsHaveField'; @@ -178,7 +178,8 @@ function OnboardingPanel({ export function Onboarding() { const api = useApi(); - const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const urlPrefix = useSelector(ConfigStore.store, state => state.urlPrefix); const project = useOnboardingProject(); const organization = useOrganization(); diff --git a/static/app/views/issueDetails/actions/shareModal.spec.tsx b/static/app/views/issueDetails/actions/shareModal.spec.tsx index 9e56cfdac5eb..fcee31426e88 100644 --- a/static/app/views/issueDetails/actions/shareModal.spec.tsx +++ b/static/app/views/issueDetails/actions/shareModal.spec.tsx @@ -15,6 +15,7 @@ describe('ShareIssueModal', () => { const onToggle = jest.fn(); beforeEach(() => { + GroupStore.reset(); GroupStore.init(); Object.assign(navigator, { clipboard: { @@ -23,7 +24,6 @@ describe('ShareIssueModal', () => { }); }); afterEach(() => { - GroupStore.reset(); MockApiClient.clearMockResponses(); jest.clearAllMocks(); }); diff --git a/static/app/views/issueDetails/actions/shareModal.tsx b/static/app/views/issueDetails/actions/shareModal.tsx index d6c9ea5e7710..c0fa979b5efe 100644 --- a/static/app/views/issueDetails/actions/shareModal.tsx +++ b/static/app/views/issueDetails/actions/shareModal.tsx @@ -2,6 +2,7 @@ import {Fragment, useRef, useState} from 'react'; import styled from '@emotion/styled'; import {IconRefresh} from '@sentry/icons/refresh'; import {useQueryClient} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import {Button} from '@sentry/scraps/button'; import {Checkbox} from '@sentry/scraps/checkbox'; @@ -15,7 +16,6 @@ import {AutoSelectText} from 'sentry/components/autoSelectText'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {t} from 'sentry/locale'; import {GroupStore} from 'sentry/stores/groupStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Event} from 'sentry/types/event'; import type {Group} from 'sentry/types/group'; import type {Organization} from 'sentry/types/organization'; @@ -60,7 +60,7 @@ export function ShareIssueModal({ ); const urlRef = useRef(null); - const groups = useLegacyStore(GroupStore); + const groups = useSelector(GroupStore.store); const group = (groups as Group[]).find(item => item.id === groupId); const api = useApi({persistInFlight: true}); const queryClient = useQueryClient(); diff --git a/static/app/views/issueDetails/groupDetails.spec.tsx b/static/app/views/issueDetails/groupDetails.spec.tsx index c103a6399397..7e0ea7c924f8 100644 --- a/static/app/views/issueDetails/groupDetails.spec.tsx +++ b/static/app/views/issueDetails/groupDetails.spec.tsx @@ -83,6 +83,9 @@ describe('groupDetails', () => { }; beforeEach(() => { + GroupStore.reset(); + ProjectsStore.reset(); + PageFiltersStore.reset(); MockApiClient.clearMockResponses(); OrganizationStore.onUpdate(defaultInit.organization); act(() => ProjectsStore.loadInitialData(defaultInit.projects)); @@ -230,9 +233,6 @@ describe('groupDetails', () => { }); afterEach(() => { - act(() => ProjectsStore.reset()); - GroupStore.reset(); - PageFiltersStore.reset(); MockApiClient.clearMockResponses(); jest.clearAllMocks(); jest.restoreAllMocks(); diff --git a/static/app/views/issueDetails/groupDetails.tsx b/static/app/views/issueDetails/groupDetails.tsx index 9638aaf09940..8f52461d6791 100644 --- a/static/app/views/issueDetails/groupDetails.tsx +++ b/static/app/views/issueDetails/groupDetails.tsx @@ -207,7 +207,7 @@ function useSyncGroupStore(groupId: string, incomingEnvs: string[]) { // It's possible the overview page is still unloading the store useEffect(() => { - return GroupStore.subscribe(() => { + return GroupStore.store.subscribe(() => { const [storeGroup] = GroupStore.getState(); if ( defined(storeGroup) && @@ -226,7 +226,7 @@ function useSyncGroupStore(groupId: string, incomingEnvs: string[]) { prev => (prev ? {...prev, json: storeGroup as Group} : prev) ); } - }); + }).unsubscribe; }, [groupId, incomingEnvs, organization, queryClient]); } diff --git a/static/app/views/issueDetails/utils.tsx b/static/app/views/issueDetails/utils.tsx index b54fe5e91496..f149ba0dd3c9 100644 --- a/static/app/views/issueDetails/utils.tsx +++ b/static/app/views/issueDetails/utils.tsx @@ -1,5 +1,6 @@ import {useMemo} from 'react'; import {useQuery} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import orderBy from 'lodash/orderBy'; import { @@ -11,7 +12,6 @@ import type {Client} from 'sentry/api'; import {t} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; import {IssueListCacheStore} from 'sentry/stores/IssueListCacheStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Event} from 'sentry/types/event'; import type {Group, GroupActivity, TagValue} from 'sentry/types/group'; import {apiOptions} from 'sentry/utils/api/apiOptions'; @@ -44,7 +44,7 @@ export async function markEventSeen( } export function useDefaultIssueEvent() { - const user = useLegacyStore(ConfigStore).user; + const user = useSelector(ConfigStore.store, state => state.user); const options = user ? user.options : null; return options?.defaultIssueEvent ?? 'recommended'; } diff --git a/static/app/views/issueList/overview.tsx b/static/app/views/issueList/overview.tsx index 3e1c390b412a..66f1263cd527 100644 --- a/static/app/views/issueList/overview.tsx +++ b/static/app/views/issueList/overview.tsx @@ -2,6 +2,7 @@ import type {ReactNode} from 'react'; import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import * as Sentry from '@sentry/react'; import {useQuery, useQueryClient} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import type {Location} from 'history'; import Cookies from 'js-cookie'; import isEqual from 'lodash/isEqual'; @@ -25,7 +26,6 @@ import {DEFAULT_STATS_PERIOD} from 'sentry/constants'; import {t, tct} from 'sentry/locale'; import {GroupStore} from 'sentry/stores/groupStore'; import {IssueListCacheStore} from 'sentry/stores/IssueListCacheStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {PageFilterDatetime} from 'sentry/types/core'; import type {BaseGroup, Group, PriorityLevel} from 'sentry/types/group'; import {GroupStatus} from 'sentry/types/group'; @@ -178,7 +178,7 @@ function IssueListOverviewInner({ const actionTakenRef = useRef(false); const {getRunIdForAnalytics} = useAiQueryContext(); - const groups = useLegacyStore(GroupStore); + const groups = useSelector(GroupStore.store); useEffect(() => { const storeGroupIds = groups.map(group => group.id).slice(0, MAX_ISSUES_COUNT); if (!isEqual(groupIds, storeGroupIds)) { diff --git a/static/app/views/issueList/pages/inbox/index.spec.tsx b/static/app/views/issueList/pages/inbox/index.spec.tsx index 019da841e11c..39cb676197e3 100644 --- a/static/app/views/issueList/pages/inbox/index.spec.tsx +++ b/static/app/views/issueList/pages/inbox/index.spec.tsx @@ -138,6 +138,7 @@ describe('InboxPage', () => { }); beforeEach(() => { + TeamStore.reset(); Element.prototype.scrollIntoView = jest.fn(); ProjectsStore.reset(); ProjectsStore.loadInitialData([project]); @@ -164,7 +165,6 @@ describe('InboxPage', () => { MockApiClient.clearMockResponses(); jest.clearAllMocks(); localStorage.removeItem('inbox-split-size'); - TeamStore.reset(); }); function mockSection( diff --git a/static/app/views/navigation/primary/helpMenu.tsx b/static/app/views/navigation/primary/helpMenu.tsx index 9c9065653b07..1c21c234f476 100644 --- a/static/app/views/navigation/primary/helpMenu.tsx +++ b/static/app/views/navigation/primary/helpMenu.tsx @@ -12,6 +12,7 @@ import {IconQuestion} from '@sentry/icons/question'; import {IconSentry} from '@sentry/icons/sentry'; import {IconSupport} from '@sentry/icons/support'; import {IconDefaultsProvider} from '@sentry/icons/useIconDefaults'; +import {useSelector} from '@tanstack/react-store'; import type {MenuItemProps} from '@sentry/scraps/dropdownMenu'; import {Flex} from '@sentry/scraps/layout'; @@ -20,7 +21,6 @@ import {openModal} from 'sentry/actionCreators/modal'; import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {t} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Organization} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; import {showIntercom} from 'sentry/utils/intercom'; @@ -44,7 +44,8 @@ export function PrimaryNavigationHelpMenu({ const organization = useOrganization(); const contactSupportItem = getContactSupportItem(organization); const openForm = useFeedbackForm(); - const {privacyUrl, termsUrl} = useLegacyStore(ConfigStore); + const privacyUrl = useSelector(ConfigStore.store, state => state.privacyUrl); + const termsUrl = useSelector(ConfigStore.store, state => state.termsUrl); const items: MenuItemProps[] = [ ...additionalItems, diff --git a/static/app/views/navigation/primary/onboarding.tsx b/static/app/views/navigation/primary/onboarding.tsx index 666dc6e983a0..896c85c8a7e6 100644 --- a/static/app/views/navigation/primary/onboarding.tsx +++ b/static/app/views/navigation/primary/onboarding.tsx @@ -2,6 +2,7 @@ import {useEffect} from 'react'; import {css, useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {IconCheckmark} from '@sentry/icons/checkmark'; +import {useSelector} from '@tanstack/react-store'; import {GuideAnchor} from 'sentry/components/assistant/guideAnchor'; import {useOnboardingSidebar} from 'sentry/components/onboarding/useOnboardingSidebar'; @@ -13,7 +14,6 @@ import { OnboardingDrawerKey, OnboardingDrawerStore, } from 'sentry/stores/onboardingDrawerStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {OnboardingTask} from 'sentry/types/onboarding'; import {trackAnalytics} from 'sentry/utils/analytics'; import {isDemoModeActive} from 'sentry/utils/demoMode'; @@ -113,7 +113,7 @@ function OnboardingItem({ } export function PrimaryNavigationOnboarding() { - const currentPanel = useLegacyStore(OnboardingDrawerStore); + const currentPanel = useSelector(OnboardingDrawerStore.store); const isActive = currentPanel === OnboardingDrawerKey.ONBOARDING_WIZARD; const organization = useOrganization(); const [quickStartCompleted, setQuickStartCompleted] = useLocalStorageState( diff --git a/static/app/views/navigation/primary/organizationDropdown.spec.tsx b/static/app/views/navigation/primary/organizationDropdown.spec.tsx index 6f2c74798005..ecddec15c27e 100644 --- a/static/app/views/navigation/primary/organizationDropdown.spec.tsx +++ b/static/app/views/navigation/primary/organizationDropdown.spec.tsx @@ -17,11 +17,8 @@ describe('OrganizationDropdown', () => { }); beforeEach(() => { - ConfigStore.set('user', UserFixture()); - }); - - afterEach(() => { ProjectsStore.reset(); + ConfigStore.set('user', UserFixture()); }); it('displays org info and links', async () => { diff --git a/static/app/views/navigation/primary/organizationDropdown.tsx b/static/app/views/navigation/primary/organizationDropdown.tsx index 075e0667a11b..71c59beec4bf 100644 --- a/static/app/views/navigation/primary/organizationDropdown.tsx +++ b/static/app/views/navigation/primary/organizationDropdown.tsx @@ -5,6 +5,7 @@ import {IconBuilding} from '@sentry/icons/building'; import {IconSettings} from '@sentry/icons/settings'; import {IconStar} from '@sentry/icons/star'; import {useMutation} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import omit from 'lodash/omit'; import orderBy from 'lodash/orderBy'; import partition from 'lodash/partition'; @@ -33,7 +34,6 @@ import {CUSTOM_REFERRER_KEY} from 'sentry/constants'; import {t, tn} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; import {OrganizationsStore} from 'sentry/stores/organizationsStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Organization, OrganizationSummary} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {isDemoModeActive} from 'sentry/utils/demoMode'; @@ -59,7 +59,10 @@ interface OrganizationDropdownProps { export function OrganizationDropdown(props: OrganizationDropdownProps) { const navigate = useNavigate(); - const config = useLegacyStore(ConfigStore); + const singleOrganization = useSelector( + ConfigStore.store, + state => state.singleOrganization + ); const theme = useTheme(); const portalContainerRef = useRef(null); @@ -69,7 +72,10 @@ export function OrganizationDropdown(props: OrganizationDropdownProps) { const organization = useOrganization(); const settingsV3 = useSettingsV3(); - const {organizations} = useLegacyStore(OrganizationsStore); + const organizations = useSelector( + OrganizationsStore.store, + state => state.organizations + ); const [activeOrgs, inactiveOrgs] = partition( organizations.filter(org => org.slug !== organization.slug), @@ -165,7 +171,7 @@ export function OrganizationDropdown(props: OrganizationDropdownProps) { leadingItems: , submenu: true, hidden: - config.singleOrganization || + singleOrganization || isDemoModeActive() || (activeOrgs.length === 0 && inactiveOrgs.length === 0), children: [ @@ -322,7 +328,7 @@ export function OrganizationDropdown(props: OrganizationDropdownProps) { key: 'switch-organization', label: t('Switch Organization'), submenu: true, - hidden: config.singleOrganization || isDemoModeActive(), + hidden: singleOrganization || isDemoModeActive(), children: [ { key: 'active-orgs', diff --git a/static/app/views/onboarding/onboarding.spec.tsx b/static/app/views/onboarding/onboarding.spec.tsx index 7d8b0e0a1dc9..a5e2f40e591d 100644 --- a/static/app/views/onboarding/onboarding.spec.tsx +++ b/static/app/views/onboarding/onboarding.spec.tsx @@ -40,6 +40,7 @@ describe('Onboarding', () => { TeamStore.loadInitialData([TeamFixture()]); }); beforeEach(() => { + ProjectsStore.reset(); MockApiClient.addMockResponse({ url: '/organizations/org-slug/config/integrations/', body: {providers: [GitHubIntegrationProviderFixture()]}, @@ -55,7 +56,6 @@ describe('Onboarding', () => { }); afterEach(() => { MockApiClient.clearMockResponses(); - ProjectsStore.reset(); sessionStorage.clear(); jest.clearAllMocks(); }); diff --git a/static/app/views/organizationContainer.tsx b/static/app/views/organizationContainer.tsx index aa6a36366349..4ea8332ad507 100644 --- a/static/app/views/organizationContainer.tsx +++ b/static/app/views/organizationContainer.tsx @@ -1,5 +1,6 @@ import {Outlet} from 'react-router'; import {useProfiler} from '@sentry/react'; +import {useSelector} from '@tanstack/react-store'; import {Alert} from '@sentry/scraps/alert'; import {Container} from '@sentry/scraps/layout'; @@ -10,7 +11,6 @@ import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {ORGANIZATION_FETCH_ERROR_TYPES} from 'sentry/constants'; import {t} from 'sentry/locale'; import {OrganizationStore} from 'sentry/stores/organizationStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; function OrganizationLoadingIndicator() { /* Track how long this component is rendered for. */ @@ -30,7 +30,9 @@ interface Props { * rendered while loading the organization. */ export function OrganizationContainer({children}: Props) { - const {loading, error, errorType} = useLegacyStore(OrganizationStore); + const loading = useSelector(OrganizationStore.store, state => state.loading); + const error = useSelector(OrganizationStore.store, state => state.error); + const errorType = useSelector(OrganizationStore.store, state => state.errorType); if (loading) { return ; diff --git a/static/app/views/organizationContext.tsx b/static/app/views/organizationContext.tsx index 2fc74088e516..91b4bc356c7f 100644 --- a/static/app/views/organizationContext.tsx +++ b/static/app/views/organizationContext.tsx @@ -1,5 +1,6 @@ import {useEffect, useRef, type ReactNode} from 'react'; import * as Sentry from '@sentry/react'; +import {useSelector} from '@tanstack/react-store'; import {switchOrganization} from 'sentry/actionCreators/organizations'; import {openSudo} from 'sentry/actionCreators/sudoModal'; @@ -15,8 +16,6 @@ import {OrganizationsStore} from 'sentry/stores/organizationsStore'; import {OrganizationStore} from 'sentry/stores/organizationStore'; import {ProjectsStore} from 'sentry/stores/projectsStore'; import {TeamStore} from 'sentry/stores/teamStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; -import type {User} from 'sentry/types/user'; import {shutdownIntercom} from 'sentry/utils/intercom'; import {OrganizationContext} from 'sentry/utils/organizationContext'; import {useParams} from 'sentry/utils/useParams'; @@ -47,11 +46,18 @@ function setRecentBootstrapTag(orgSlug: string) { * OrganizationStore if it is not already present. */ export function OrganizationContextProvider({children}: Props) { - const configStore = useLegacyStore(ConfigStore); - const {organizations} = useLegacyStore(OrganizationsStore); - const {organization, error} = useLegacyStore(OrganizationStore); - const lastOrganizationSlug = - configStore.lastOrganization ?? organizations[0]?.slug ?? null; + const lastOrganization = useSelector( + ConfigStore.store, + state => state.lastOrganization + ); + const user = useSelector(ConfigStore.store, state => state.user); + const organizations = useSelector( + OrganizationsStore.store, + state => state.organizations + ); + const organization = useSelector(OrganizationStore.store, state => state.organization); + const error = useSelector(OrganizationStore.store, state => state.error); + const lastOrganizationSlug = lastOrganization ?? organizations[0]?.slug ?? null; const params = useParams<{orgId?: string}>(); const spanRef = useRef(null); @@ -100,7 +106,6 @@ export function OrganizationContextProvider({children}: Props) { // XXX(epurkhiser): User may be null in some scenarios at this point in app // boot. We should fix the types here in the future - const user: User | null = configStore.user; // It may be possible for the user to use the sudo modal to load the organization. useEffect(() => { diff --git a/static/app/views/organizationCreate/index.tsx b/static/app/views/organizationCreate/index.tsx index 00372c1da922..538cac6f3c4c 100644 --- a/static/app/views/organizationCreate/index.tsx +++ b/static/app/views/organizationCreate/index.tsx @@ -1,4 +1,5 @@ import {useMutation} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import {z} from 'zod'; import {Checkbox} from '@sentry/scraps/checkbox'; @@ -16,7 +17,6 @@ import {NarrowLayout} from 'sentry/components/narrowLayout'; import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle'; import {t, tct} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {OrganizationSummary} from 'sentry/types/organization'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {getSignupLocalities} from 'sentry/utils/cells'; @@ -60,8 +60,11 @@ const schema = z.object({ }); function OrganizationCreate() { - const {termsUrl, privacyUrl, isSelfHosted, features, links} = - useLegacyStore(ConfigStore); + const termsUrl = useSelector(ConfigStore.store, state => state.termsUrl); + const privacyUrl = useSelector(ConfigStore.store, state => state.privacyUrl); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const features = useSelector(ConfigStore.store, state => state.features); + const links = useSelector(ConfigStore.store, state => state.links); const relocationUrl = normalizeUrl('/relocation/'); const localityOptions = getSignupLocalities(); diff --git a/static/app/views/organizationStats/index.spec.tsx b/static/app/views/organizationStats/index.spec.tsx index 8916857a0c8e..509914af6f5a 100644 --- a/static/app/views/organizationStats/index.spec.tsx +++ b/static/app/views/organizationStats/index.spec.tsx @@ -33,6 +33,7 @@ describe('OrganizationStats', () => { let mockRequest: jest.Mock; beforeEach(() => { + PageFiltersStore.reset(); MockApiClient.clearMockResponses(); PageFiltersStore.init(); PageFiltersStore.onInitializeUrlState(defaultSelection); @@ -47,10 +48,6 @@ describe('OrganizationStats', () => { ConfigStore.set('user', UserFixture()); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - /** * Features and Alerts */ diff --git a/static/app/views/performance/eap/segmentSpansTable.spec.tsx b/static/app/views/performance/eap/segmentSpansTable.spec.tsx index 6df55a4d7c6c..e13d85c9f326 100644 --- a/static/app/views/performance/eap/segmentSpansTable.spec.tsx +++ b/static/app/views/performance/eap/segmentSpansTable.spec.tsx @@ -14,6 +14,7 @@ describe('SegmentSpansTable', () => { const project = ProjectFixture({id: '1'}); beforeEach(() => { + ProjectsStore.reset(); MockApiClient.clearMockResponses(); PageFiltersStore.init(); PageFiltersStore.onInitializeUrlState( @@ -22,10 +23,6 @@ describe('SegmentSpansTable', () => { ProjectsStore.loadInitialData([project]); }); - afterEach(() => { - ProjectsStore.reset(); - }); - it('renders column headers and data rows', async () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, diff --git a/static/app/views/performance/onboarding.spec.tsx b/static/app/views/performance/onboarding.spec.tsx index 149e834be88e..281574c6dc7c 100644 --- a/static/app/views/performance/onboarding.spec.tsx +++ b/static/app/views/performance/onboarding.spec.tsx @@ -42,6 +42,7 @@ describe('Testing new onboarding ui', () => { const organization = OrganizationFixture(); beforeEach(() => { + PageFiltersStore.reset(); MockApiClient.addMockResponse({ url: '/projects/org-slug/project-slug/keys/', method: 'GET', @@ -56,10 +57,6 @@ describe('Testing new onboarding ui', () => { PageFiltersStore.init(); }); - afterEach(() => { - PageFiltersStore.reset(); - }); - it('Renders updated ui', async () => { const projectMock = ProjectFixture({ platform: 'javascript-react', diff --git a/static/app/views/performance/onboarding.tsx b/static/app/views/performance/onboarding.tsx index 678b78027a6a..8cf57be1ee90 100644 --- a/static/app/views/performance/onboarding.tsx +++ b/static/app/views/performance/onboarding.tsx @@ -2,6 +2,7 @@ import {Fragment, useEffect} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {useQuery} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import emptyStateImg from 'sentry-images/spot/performance-empty-state.svg'; import emptyTraceImg from 'sentry-images/spot/performance-empty-trace.svg'; @@ -56,7 +57,6 @@ import { OnboardingDrawerKey, OnboardingDrawerStore, } from 'sentry/stores/onboardingDrawerStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {pulsingIndicatorStyles} from 'sentry/styles/pulsingIndicator'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; @@ -414,7 +414,8 @@ export function Onboarding({organization, project}: OnboardingProps) { const api = useApi(); const location = useLocation(); const navigate = useNavigate(); - const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); + const urlPrefix = useSelector(ConfigStore.store, state => state.urlPrefix); const noPerformanceSupport = doesNotSupportPerformance(project); diff --git a/static/app/views/performance/traceDetails/trace.tsx b/static/app/views/performance/traceDetails/trace.tsx index b8e3fe48e081..8e6b772176cb 100644 --- a/static/app/views/performance/traceDetails/trace.tsx +++ b/static/app/views/performance/traceDetails/trace.tsx @@ -10,11 +10,11 @@ import { } from 'react'; import {css, useTheme, type Theme} from '@emotion/react'; import styled from '@emotion/styled'; +import {useSelector} from '@tanstack/react-store'; import {Tooltip} from '@sentry/scraps/tooltip'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -154,7 +154,7 @@ export function Trace({ const organization = useOrganization(); const traceState = useTraceState(); const traceDispatch = useTraceStateDispatch(); - const {theme: colorMode} = useLegacyStore(ConfigStore); + const colorMode = useSelector(ConfigStore.store, state => state.theme); const rerenderRef = useRef(rerender); rerenderRef.current = rerender; diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTab.spec.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTab.spec.tsx index 27805c953a9f..d17a9606613f 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTab.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTab.spec.tsx @@ -17,6 +17,7 @@ describe('EAPSampledEventsTab', () => { }); beforeEach(() => { + ProjectsStore.reset(); ProjectsStore.loadInitialData([project]); PageFiltersStore.onInitializeUrlState( PageFiltersFixture({projects: [Number(project.id)]}) @@ -101,7 +102,6 @@ describe('EAPSampledEventsTab', () => { afterEach(() => { MockApiClient.clearMockResponses(); - ProjectsStore.reset(); }); it('renders the filter bar and table', async () => { diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.spec.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.spec.tsx index 72a874e5f957..024e3fa85950 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.spec.tsx @@ -70,6 +70,7 @@ describe('EAP SampledEventsTable', () => { ); beforeEach(() => { + ProjectsStore.reset(); ProjectsStore.loadInitialData([project]); PageFiltersStore.onInitializeUrlState( PageFiltersFixture({projects: [Number(project.id)]}) @@ -88,7 +89,6 @@ describe('EAP SampledEventsTable', () => { afterEach(() => { MockApiClient.clearMockResponses(); - ProjectsStore.reset(); }); it('renders the table with data', async () => { diff --git a/static/app/views/projectDetail/projectDetail.spec.tsx b/static/app/views/projectDetail/projectDetail.spec.tsx index 07a2550f1127..48de690b5ebe 100644 --- a/static/app/views/projectDetail/projectDetail.spec.tsx +++ b/static/app/views/projectDetail/projectDetail.spec.tsx @@ -74,14 +74,11 @@ describe('ProjectDetail', () => { } beforeEach(() => { + ProjectsStore.reset(); MockApiClient.clearMockResponses(); ProjectsStore.init(); }); - afterEach(() => { - ProjectsStore.reset(); - }); - it('Render an error if project not found', async () => { ProjectsStore.loadInitialData([{...project, slug: 'different-slug'}]); setupMockResponses(); diff --git a/static/app/views/projectsDashboard/index.spec.tsx b/static/app/views/projectsDashboard/index.spec.tsx index 3073cda95631..5f87b1caa0dd 100644 --- a/static/app/views/projectsDashboard/index.spec.tsx +++ b/static/app/views/projectsDashboard/index.spec.tsx @@ -20,6 +20,7 @@ describe('ProjectsDashboard', () => { const teams = [team]; beforeEach(() => { + TeamStore.reset(); TeamStore.loadInitialData(teams); MockApiClient.addMockResponse({ url: `/teams/${org.slug}/${team.slug}/members/`, @@ -33,7 +34,6 @@ describe('ProjectsDashboard', () => { }); afterEach(() => { - TeamStore.reset(); MockApiClient.clearMockResponses(); }); diff --git a/static/app/views/routeError.tsx b/static/app/views/routeError.tsx index ed8241ea413a..6916333c5b5c 100644 --- a/static/app/views/routeError.tsx +++ b/static/app/views/routeError.tsx @@ -3,6 +3,7 @@ import {useMatches} from 'react-router'; import styled from '@emotion/styled'; import type {Scope} from '@sentry/core'; import * as Sentry from '@sentry/react'; +import {useSelector} from '@tanstack/react-store'; import {Alert} from '@sentry/scraps/alert'; import {ExternalLink} from '@sentry/scraps/link'; @@ -12,7 +13,6 @@ import {List} from 'sentry/components/list'; import {ListItem} from 'sentry/components/list/listItem'; import {t, tct} from 'sentry/locale'; import {OrganizationStore} from 'sentry/stores/organizationStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {getRouteStringFromRoutes} from 'sentry/utils/getRouteStringFromRoutes'; import {ProjectRouteContext} from 'sentry/views/projects/projectRouteContext'; @@ -22,7 +22,7 @@ interface RouteErrorProps { export function RouteError({error}: RouteErrorProps) { const matches = useMatches(); - const {organization} = useLegacyStore(OrganizationStore); + const organization = useSelector(OrganizationStore.store, state => state.organization); const project = useContext(ProjectRouteContext); useEffect(() => { diff --git a/static/app/views/settings/account/accountNotificationFineTuning.tsx b/static/app/views/settings/account/accountNotificationFineTuning.tsx index dd31753b5e02..f4eb702d2f81 100644 --- a/static/app/views/settings/account/accountNotificationFineTuning.tsx +++ b/static/app/views/settings/account/accountNotificationFineTuning.tsx @@ -1,5 +1,6 @@ import {Fragment} from 'react'; import {useQuery, keepPreviousData} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import {parseAsString, useQueryState} from 'nuqs'; import {z} from 'zod'; @@ -18,7 +19,6 @@ import {SearchBar} from 'sentry/components/searchBar'; import {t} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; import {OrganizationsStore} from 'sentry/stores/organizationsStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Project} from 'sentry/types/project'; import type {UserEmail} from 'sentry/types/user'; import {apiOptions, selectJsonWithHeaders} from 'sentry/utils/api/apiOptions'; @@ -100,8 +100,11 @@ function AccountNotificationsByProject({ } export function AccountNotificationFineTuning() { - const {organizations} = useLegacyStore(OrganizationsStore); - const config = useLegacyStore(ConfigStore); + const organizations = useSelector( + OrganizationsStore.store, + state => state.organizations + ); + const customerDomain = useSelector(ConfigStore.store, state => state.customerDomain); const [orgIdParam, setOrgId] = useQueryState('organizationId', parseAsString); const [cursor] = useQueryState('cursor', parseAsString); @@ -113,7 +116,7 @@ export function AccountNotificationFineTuning() { // - default to first org const organizationId = orgIdParam ?? - organizations.find(({slug}) => slug === config?.customerDomain?.subdomain)?.id ?? + organizations.find(({slug}) => slug === customerDomain?.subdomain)?.id ?? (organizations.length === 1 ? organizations[0]?.id : undefined); const { diff --git a/static/app/views/settings/account/accountNotificationFineTuningController.tsx b/static/app/views/settings/account/accountNotificationFineTuningController.tsx index ffe766bef918..5f087c0a26bf 100644 --- a/static/app/views/settings/account/accountNotificationFineTuningController.tsx +++ b/static/app/views/settings/account/accountNotificationFineTuningController.tsx @@ -1,6 +1,7 @@ +import {useSelector} from '@tanstack/react-store'; + import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {OrganizationsStore} from 'sentry/stores/organizationsStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {useParams} from 'sentry/utils/useParams'; import {NotificationSettingsByType} from 'sentry/views/settings/account/notifications/notificationSettingsByType'; import {getNotificationTypeFromPathname} from 'sentry/views/settings/account/notifications/utils'; @@ -19,7 +20,7 @@ const accountNotifications = [ ]; export default function AccountNotificationFineTuningController() { - const {loaded} = useLegacyStore(OrganizationsStore); + const loaded = useSelector(OrganizationsStore.store, state => state.loaded); const {fineTuneType: pathnameType} = useParams<{fineTuneType: string}>(); const fineTuneType = getNotificationTypeFromPathname(pathnameType); diff --git a/static/app/views/settings/account/accountSecurity/accountSecurityWrapper.tsx b/static/app/views/settings/account/accountSecurity/accountSecurityWrapper.tsx index ca04ef068603..1b6c4e176367 100644 --- a/static/app/views/settings/account/accountSecurity/accountSecurityWrapper.tsx +++ b/static/app/views/settings/account/accountSecurity/accountSecurityWrapper.tsx @@ -1,13 +1,13 @@ import {useCallback} from 'react'; import {Outlet, useOutletContext} from 'react-router'; import {useMutation, useQuery} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; import {LoadingError} from 'sentry/components/loadingError'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {t} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Authenticator} from 'sentry/types/auth'; import type {OrganizationSummary} from 'sentry/types/organization'; import type {UserEmail} from 'sentry/types/user'; @@ -21,7 +21,7 @@ const ENDPOINT = getApiUrl('/users/$userId/authenticators/', {path: {userId: 'me export default function AccountSecurityWrapper() { const api = useApi(); const {authId} = useParams<{authId?: string}>(); - const {links} = useLegacyStore(ConfigStore); + const links = useSelector(ConfigStore.store, state => state.links); const orgRequest = useQuery( apiOptions.as()('/organizations/', { diff --git a/static/app/views/settings/account/notifications/notificationSettingsByType.spec.tsx b/static/app/views/settings/account/notifications/notificationSettingsByType.spec.tsx index 711786f11fe3..401e03053a39 100644 --- a/static/app/views/settings/account/notifications/notificationSettingsByType.spec.tsx +++ b/static/app/views/settings/account/notifications/notificationSettingsByType.spec.tsx @@ -93,6 +93,7 @@ function renderComponent({ describe('NotificationSettingsByType', () => { beforeEach(() => { + OrganizationsStore.init(); OrganizationsStore.init(); MockApiClient.addMockResponse({ url: '/notification-defaults/', @@ -103,7 +104,6 @@ describe('NotificationSettingsByType', () => { afterEach(() => { MockApiClient.clearMockResponses(); - OrganizationsStore.init(); jest.clearAllMocks(); }); it('should render when default is disabled', async () => { diff --git a/static/app/views/settings/account/notifications/notificationSettingsByType.tsx b/static/app/views/settings/account/notifications/notificationSettingsByType.tsx index 8b34a8fd844c..65c74e0884b4 100644 --- a/static/app/views/settings/account/notifications/notificationSettingsByType.tsx +++ b/static/app/views/settings/account/notifications/notificationSettingsByType.tsx @@ -5,6 +5,7 @@ import { useMutation, useQueryClient, } from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import {z} from 'zod'; import {AutoSaveForm, FieldGroup} from '@sentry/scraps/form'; @@ -15,7 +16,6 @@ import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle'; import {t} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; import {OrganizationsStore} from 'sentry/stores/organizationsStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {OrganizationIntegration} from 'sentry/types/integrations'; import type {OrganizationSummary} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -132,7 +132,10 @@ function DefaultNotificationField({ } export function NotificationSettingsByType({notificationType}: Props) { - const {organizations} = useLegacyStore(OrganizationsStore); + const organizations = useSelector( + OrganizationsStore.store, + state => state.organizations + ); const queryClient = useQueryClient(); const {data: notificationOptions = [], status: notificationOptionStatus} = useApiQuery< NotificationOptionsObject[] diff --git a/static/app/views/settings/account/notifications/notificationSettingsController.tsx b/static/app/views/settings/account/notifications/notificationSettingsController.tsx index 623f7d618ced..3922bd050974 100644 --- a/static/app/views/settings/account/notifications/notificationSettingsController.tsx +++ b/static/app/views/settings/account/notifications/notificationSettingsController.tsx @@ -1,11 +1,12 @@ +import {useSelector} from '@tanstack/react-store'; + import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {OrganizationsStore} from 'sentry/stores/organizationsStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {NotificationSettings} from './notificationSettings'; function NotificationSettingsController() { - const {loaded} = useLegacyStore(OrganizationsStore); + const loaded = useSelector(OrganizationsStore.store, state => state.loaded); if (!loaded) { return ; } diff --git a/static/app/views/settings/earlyFeatures/index.tsx b/static/app/views/settings/earlyFeatures/index.tsx index fe7b83883022..2c82c9ee58d6 100644 --- a/static/app/views/settings/earlyFeatures/index.tsx +++ b/static/app/views/settings/earlyFeatures/index.tsx @@ -1,18 +1,18 @@ import {Fragment} from 'react'; +import {useSelector} from '@tanstack/react-store'; import {FeatureBadge} from '@sentry/scraps/badge'; import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle'; import {t} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {useOrganization} from 'sentry/utils/useOrganization'; import {SettingsPageHeader} from 'sentry/views/settings/components/settingsPageHeader'; import {EarlyFeaturesSettingsForm} from 'sentry/views/settings/earlyFeatures/settingsForm'; import {OrganizationPermissionAlert} from 'sentry/views/settings/organization/organizationPermissionAlert'; export default function OrganizationGeneralSettings() { - const {isSelfHosted} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); const organization = useOrganization(); const access = new Set(organization.access); diff --git a/static/app/views/settings/organizationIntegrations/integrationCodeMappings.spec.tsx b/static/app/views/settings/organizationIntegrations/integrationCodeMappings.spec.tsx index 029db5142135..3e215180d2df 100644 --- a/static/app/views/settings/organizationIntegrations/integrationCodeMappings.spec.tsx +++ b/static/app/views/settings/organizationIntegrations/integrationCodeMappings.spec.tsx @@ -59,6 +59,7 @@ describe('IntegrationCodeMappings', () => { }); beforeEach(() => { + ProjectsStore.reset(); ProjectsStore.loadInitialData(projects); MockApiClient.addMockResponse({ @@ -76,7 +77,6 @@ describe('IntegrationCodeMappings', () => { }); afterEach(() => { - ProjectsStore.reset(); MockApiClient.clearMockResponses(); }); diff --git a/static/app/views/settings/organizationMembers/inviteRequestRow.spec.tsx b/static/app/views/settings/organizationMembers/inviteRequestRow.spec.tsx index 448964f9e277..91ae98279bcc 100644 --- a/static/app/views/settings/organizationMembers/inviteRequestRow.spec.tsx +++ b/static/app/views/settings/organizationMembers/inviteRequestRow.spec.tsx @@ -41,6 +41,10 @@ const roles: OrgRole[] = [ ]; describe('InviteRequestRow', () => { + beforeEach(() => { + TeamStore.reset(); + }); + const orgWithoutAdminAccess = OrganizationFixture({ access: [], }); @@ -221,8 +225,6 @@ describe('InviteRequestRow', () => { container: document.body, }); expect(mockUpdate).toHaveBeenCalledWith({teams: ['myteam', 'one']}); - - TeamStore.reset(); }); it('cannot be approved when invitee role is not allowed', () => { diff --git a/static/app/views/settings/organizationTeams/organizationTeams.spec.tsx b/static/app/views/settings/organizationTeams/organizationTeams.spec.tsx index d917dc873e78..ea5f491877c3 100644 --- a/static/app/views/settings/organizationTeams/organizationTeams.spec.tsx +++ b/static/app/views/settings/organizationTeams/organizationTeams.spec.tsx @@ -25,11 +25,13 @@ jest.mock('sentry/actionCreators/modal', () => ({ const TEAM_SEARCH = 'frontend'; describe('OrganizationTeams', () => { - beforeEach(() => TeamStore.loadInitialData([], false, null)); + beforeEach(() => { + TeamStore.reset(); + TeamStore.loadInitialData([], false, null); + }); afterEach(() => { jest.useRealTimers(); - act(() => TeamStore.reset()); }); it('debounces team search requests', async () => { diff --git a/static/app/views/settings/organizationTeams/teamDetails.spec.tsx b/static/app/views/settings/organizationTeams/teamDetails.spec.tsx index 57bcfc93b514..588312d6f513 100644 --- a/static/app/views/settings/organizationTeams/teamDetails.spec.tsx +++ b/static/app/views/settings/organizationTeams/teamDetails.spec.tsx @@ -14,6 +14,7 @@ describe('TeamDetails', () => { const teamHasAccess = TeamFixture({id: '1337', slug: 'django', hasAccess: true}); beforeEach(() => { + TeamStore.reset(); TeamStore.init(); TeamStore.loadInitialData([teamNoAccess, teamHasAccess]); joinMock = MockApiClient.addMockResponse({ @@ -24,7 +25,6 @@ describe('TeamDetails', () => { afterEach(() => { MockApiClient.clearMockResponses(); - TeamStore.reset(); }); it('can request membership', async () => { diff --git a/static/app/views/settings/projectGeneralSettings/index.tsx b/static/app/views/settings/projectGeneralSettings/index.tsx index 7b39355fc845..2d7412b7221b 100644 --- a/static/app/views/settings/projectGeneralSettings/index.tsx +++ b/static/app/views/settings/projectGeneralSettings/index.tsx @@ -1,5 +1,6 @@ import {useCallback, useMemo} from 'react'; import {useMutation} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import {PlatformIcon} from 'platformicons'; import {z} from 'zod'; @@ -42,7 +43,6 @@ import {consoles} from 'sentry/data/platformCategories'; import {allPlatforms as platforms} from 'sentry/data/platforms'; import {t, tct, tn} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Organization} from 'sentry/types/organization'; import type {PlatformKey} from 'sentry/types/platform'; import type {DetailedProject} from 'sentry/types/project'; @@ -555,7 +555,7 @@ function SecurityTokenHeaderForm({ export function ProjectGeneralSettings({project, onChangeSlug}: Props) { const transferForm: Record = {}; const navigate = useNavigate(); - const {isSelfHosted} = useLegacyStore(ConfigStore); + const isSelfHosted = useSelector(ConfigStore.store, state => state.isSelfHosted); const organization = useOrganization(); const api = useApi({persistInFlight: true}); diff --git a/static/app/views/setupWizard/utils/useOrganizationsWithRegion.tsx b/static/app/views/setupWizard/utils/useOrganizationsWithRegion.tsx index 730843386825..821af509615e 100644 --- a/static/app/views/setupWizard/utils/useOrganizationsWithRegion.tsx +++ b/static/app/views/setupWizard/utils/useOrganizationsWithRegion.tsx @@ -1,8 +1,8 @@ import * as Sentry from '@sentry/react'; import {useQuery} from '@tanstack/react-query'; +import {useSelector} from '@tanstack/react-store'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {OrganizationSummary} from 'sentry/types/organization'; import type {Locality} from 'sentry/types/system'; import {apiOptions} from 'sentry/utils/api/apiOptions'; @@ -15,7 +15,7 @@ export function useOrganizationsWithRegion() { acc[l.url] = l; return acc; }, {}); - const {links} = useLegacyStore(ConfigStore); + const links = useSelector(ConfigStore.store, state => state.links); const query = useQuery({ ...apiOptions.as()('/organizations/', { diff --git a/static/gsApp/components/stripeWrapper.tsx b/static/gsApp/components/stripeWrapper.tsx index 4c2a29817ee0..b3bc47d9ccdc 100644 --- a/static/gsApp/components/stripeWrapper.tsx +++ b/static/gsApp/components/stripeWrapper.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {useTheme} from '@emotion/react'; import {Elements} from '@stripe/react-stripe-js'; +import {useSelector} from '@tanstack/react-store'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {useStripeInstance} from 'getsentry/hooks/useStripeInstance'; @@ -18,7 +18,7 @@ export function StripeWrapper({ }) { const stripe = useStripeInstance(); const theme = useTheme(); - const prefersDarkMode = useLegacyStore(ConfigStore).theme === 'dark'; + const prefersDarkMode = useSelector(ConfigStore.store, state => state.theme) === 'dark'; // NOTE: These need to match what we set in the backend // for payment and setup intents diff --git a/static/gsApp/components/trialStartedSidebarItem.tsx b/static/gsApp/components/trialStartedSidebarItem.tsx index d15299e2034a..87b9b38317dd 100644 --- a/static/gsApp/components/trialStartedSidebarItem.tsx +++ b/static/gsApp/components/trialStartedSidebarItem.tsx @@ -111,7 +111,7 @@ function TrialStartedSidebarItem({subscription, organization, children}: Props) ); useEffect(() => { - const unsubscribe = TrialRequestedStore.subscribe(() => + const {unsubscribe} = TrialRequestedStore.store.subscribe(() => setTrialRequested(TrialRequestedStore.getTrialRequstedState()) ); return () => { diff --git a/static/gsApp/components/withSubscription.tsx b/static/gsApp/components/withSubscription.tsx index 9868a970658f..3a67c6f4217b 100644 --- a/static/gsApp/components/withSubscription.tsx +++ b/static/gsApp/components/withSubscription.tsx @@ -78,14 +78,14 @@ export function withSubscription

( } private prevSubscriptions = SubscriptionStore.getState(); - unsubscribe = SubscriptionStore.subscribe(subscriptions => { + unsubscribe = SubscriptionStore.store.subscribe(subscriptions => { const orgSlug = this.getOrgSlug(); const prevSubscriptions = this.prevSubscriptions; this.prevSubscriptions = subscriptions; if (orgSlug !== null && subscriptions[orgSlug] !== prevSubscriptions[orgSlug]) { this.onSubscriptionChange(subscriptions[orgSlug]!); } - }); + }).unsubscribe; private mounted = false; configureScopeWithSubscriptionData(subscription: Subscription) { diff --git a/static/gsApp/hooks/useSubscription.tsx b/static/gsApp/hooks/useSubscription.tsx index 67a7df687d4b..e90ee424504f 100644 --- a/static/gsApp/hooks/useSubscription.tsx +++ b/static/gsApp/hooks/useSubscription.tsx @@ -1,4 +1,5 @@ -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; +import {useSelector} from '@tanstack/react-store'; + import {useOrganization} from 'sentry/utils/useOrganization'; import {SubscriptionStore} from 'getsentry/stores/subscriptionStore'; @@ -6,5 +7,5 @@ import type {Subscription} from 'getsentry/types'; export function useSubscription(): Subscription | null { const organization = useOrganization(); - return useLegacyStore(SubscriptionStore)[organization.slug] ?? null; + return useSelector(SubscriptionStore.store, state => state[organization.slug]) ?? null; } diff --git a/static/gsApp/overrides/useBillingNavigationConfig.tsx b/static/gsApp/overrides/useBillingNavigationConfig.tsx index 11a43dbe21e9..303ab27ba3ec 100644 --- a/static/gsApp/overrides/useBillingNavigationConfig.tsx +++ b/static/gsApp/overrides/useBillingNavigationConfig.tsx @@ -1,7 +1,7 @@ import {useMemo} from 'react'; +import {useSelector} from '@tanstack/react-store'; import {t} from 'sentry/locale'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Scope} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -15,7 +15,8 @@ type NavProps = NavigationProps & { export function useBillingNavigationConfig(): NavigationSection | null { const organization = useOrganization(); - const subscription = useLegacyStore(SubscriptionStore)[organization.slug] ?? null; + const subscription = + useSelector(SubscriptionStore.store, state => state[organization.slug]) ?? null; return useMemo(() => { if (!subscription) { diff --git a/static/gsApp/stores/subscriptionStore.tsx b/static/gsApp/stores/subscriptionStore.tsx index c872887edad6..a875aa51912e 100644 --- a/static/gsApp/stores/subscriptionStore.tsx +++ b/static/gsApp/stores/subscriptionStore.tsx @@ -1,5 +1,6 @@ +import {Store} from '@tanstack/react-store'; + import {Client} from 'sentry/api'; -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; import type {Subscription} from 'getsentry/types'; @@ -14,10 +15,14 @@ type LoadOptions = { */ type Subscriptions = Record; -const store = createLegacyStore({}); +const store = new Store({}); export const SubscriptionStore = { - ...store, + store, + + getState() { + return store.state; + }, api: new Client(), loadingData: {} as Record, callbacks: {} as Record, @@ -26,12 +31,12 @@ export const SubscriptionStore = { SubscriptionStore.api = new Client(); SubscriptionStore.loadingData = {}; SubscriptionStore.callbacks = {}; - store.resetState({}); + store.setState(() => ({})); }, set(orgSlug: string, data: Partial) { const subscription = { - ...store.getState()[orgSlug]!, + ...store.state[orgSlug]!, ...data, setAt: Date.now(), // Refetch usage data if Subscription is updated }; @@ -46,7 +51,7 @@ export const SubscriptionStore = { }, get(orgSlug: string, callback: GetCallback) { - const subscription = store.getState()[orgSlug]; + const subscription = store.state[orgSlug]; if (subscription) { callback(subscription); return; @@ -56,7 +61,7 @@ export const SubscriptionStore = { }, clearStartedTrial(orgSlug: string) { - const current = store.getState()[orgSlug]; + const current = store.state[orgSlug]; if (!current) { return; } diff --git a/static/gsApp/stores/trialRequestedStore.tsx b/static/gsApp/stores/trialRequestedStore.tsx index 9250db1e9bed..ff5cdeb2d988 100644 --- a/static/gsApp/stores/trialRequestedStore.tsx +++ b/static/gsApp/stores/trialRequestedStore.tsx @@ -1,13 +1,17 @@ -import {createLegacyStore} from 'sentry/stores/createLegacyStore'; +import {Store} from '@tanstack/react-store'; type State = { requested: boolean; }; -const store = createLegacyStore({requested: false}); +const store = new Store({requested: false}); export const TrialRequestedStore = { - ...store, + store, + + getState() { + return store.state; + }, onRequested() { store.setState(prev => ({...prev, requested: true})); @@ -18,6 +22,6 @@ export const TrialRequestedStore = { }, getTrialRequstedState() { - return store.getState().requested; + return store.state.requested; }, }; diff --git a/static/gsApp/views/amCheckout/components/cartDiff.tsx b/static/gsApp/views/amCheckout/components/cartDiff.tsx index 6440f070d29d..ba21ebcfdd07 100644 --- a/static/gsApp/views/amCheckout/components/cartDiff.tsx +++ b/static/gsApp/views/amCheckout/components/cartDiff.tsx @@ -1,6 +1,7 @@ import React, {Fragment, useCallback, useMemo} from 'react'; import styled from '@emotion/styled'; import {IconChevron} from '@sentry/icons/chevron'; +import {useSelector} from '@tanstack/react-store'; // eslint-disable-next-line no-restricted-imports import color from 'color'; import isEqual from 'lodash/isEqual'; @@ -11,7 +12,6 @@ import {Heading, Text} from '@sentry/scraps/text'; import {t, tct} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {DataCategory} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import {capitalize} from 'sentry/utils/string/capitalize'; @@ -61,7 +61,7 @@ type SharedOnDemandChange = CheckoutChange<'sharedMaxBudget', number>; type PerCategoryOnDemandChange = CheckoutChange; function AddedHighlight({value}: {value: string}) { - const prefersDarkMode = useLegacyStore(ConfigStore).theme === 'dark'; + const prefersDarkMode = useSelector(ConfigStore.store, state => state.theme) === 'dark'; return ( {value} @@ -70,7 +70,7 @@ function AddedHighlight({value}: {value: string}) { } function RemovedHighlight({value}: {value: string}) { - const prefersDarkMode = useLegacyStore(ConfigStore).theme === 'dark'; + const prefersDarkMode = useSelector(ConfigStore.store, state => state.theme) === 'dark'; return ( {value} diff --git a/static/gsApp/views/spendAllocations/components/index.spec.tsx b/static/gsApp/views/spendAllocations/components/index.spec.tsx index bdf522ba0023..f6774e80bdb7 100644 --- a/static/gsApp/views/spendAllocations/components/index.spec.tsx +++ b/static/gsApp/views/spendAllocations/components/index.spec.tsx @@ -10,11 +10,8 @@ describe('projectSelectControl', () => { const {projects} = initializeOrg(); beforeEach(() => { - ProjectsStore.loadInitialData(projects); - }); - - afterEach(() => { ProjectsStore.reset(); + ProjectsStore.loadInitialData(projects); }); it('removes options that are filtered out', async () => { From 4af288201055de7836c7b6b799fcbd78b68fa3d3 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Tue, 27 Oct 2026 15:52:53 -0700 Subject: [PATCH 4/7] chore(stores): Drop deleted hook from codeowners baseline Co-Authored-By: Claude Opus 5.5 --- .github/codeowners-coverage-baseline.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/codeowners-coverage-baseline.txt b/.github/codeowners-coverage-baseline.txt index 84ec06acf222..7e4d1372c5c2 100644 --- a/.github/codeowners-coverage-baseline.txt +++ b/.github/codeowners-coverage-baseline.txt @@ -1222,8 +1222,6 @@ static/app/stores/tagStore.tsx static/app/stores/teamStore.spec.tsx static/app/stores/teamStore.tsx static/app/stores/types.tsx -static/app/stores/useLegacyStore.spec.tsx -static/app/stores/useLegacyStore.tsx static/app/stores/useSentryAppComponentsData.tsx static/app/styles/animations.tsx static/app/styles/global.tsx From f67ab448a8bd6dfae5e22374551dd378a4639f7c Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Tue, 27 Oct 2026 15:53:45 -0700 Subject: [PATCH 5/7] docs(stores): Drop reflux from the forms skill Co-Authored-By: Claude Opus 5.5 --- .agents/skills/generate-frontend-forms/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/generate-frontend-forms/SKILL.md b/.agents/skills/generate-frontend-forms/SKILL.md index 8d9b5f8f18f6..22895f52212a 100644 --- a/.agents/skills/generate-frontend-forms/SKILL.md +++ b/.agents/skills/generate-frontend-forms/SKILL.md @@ -9,7 +9,7 @@ This skill provides patterns for building forms using Sentry's new form system b ## Core Principle -- Always use the new form system (`useScrapsForm`, `AutoSaveForm`) for new forms. Never create new forms with the legacy JsonForm or Reflux-based systems. +- Always use the new form system (`useScrapsForm`, `AutoSaveForm`) for new forms. Never create new forms with the legacy JsonForm system. - All forms should be schema based. DO NOT create a form without schema validation. From 9108ea6b3182277b29dbd53f44352bad45efddf6 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Tue, 27 Oct 2026 15:59:29 -0700 Subject: [PATCH 6/7] chore(stores): Only depend on tanstack store in app workspaces The root package never imports it, which knip flags. Co-Authored-By: Claude Opus 5.5 --- package.json | 1 - pnpm-lock.yaml | 3 --- 2 files changed, 4 deletions(-) diff --git a/package.json b/package.json index fddec9b8ee2e..03937a509508 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,6 @@ "@sentry/sqlish": "1.0.1", "@sentry/webpack-plugin": "5.4.1", "@swc/plugin-emotion": "19.0.0", - "@tanstack/react-store": "0.11.2", "@types/gtag.js": "^0.0.12", "@types/jest": "30.0.0", "@types/react": "19.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f44f1a251ae..56ba3dffc4af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -241,9 +241,6 @@ importers: '@swc/plugin-emotion': specifier: 19.0.0 version: 19.0.0 - '@tanstack/react-store': - specifier: 0.11.2 - version: 0.11.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@types/gtag.js': specifier: ^0.0.12 version: 0.0.12 From 2c913fe91450b58aeaeb885101c49879a0170882 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Tue, 27 Oct 2026 16:08:11 -0700 Subject: [PATCH 7/7] test(discover): Wait for transactions list pagination 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 --- static/app/components/discover/transactionsList.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/discover/transactionsList.spec.tsx b/static/app/components/discover/transactionsList.spec.tsx index 6748e0ab9c4b..d381d8dfc428 100644 --- a/static/app/components/discover/transactionsList.spec.tsx +++ b/static/app/components/discover/transactionsList.spec.tsx @@ -170,7 +170,7 @@ describe('TransactionsList', () => { expect( screen.getByRole('button', {name: 'Filter Transactions'}) ).toBeInTheDocument(); - expect(screen.getByRole('button', {name: 'Previous'})).toBeInTheDocument(); + expect(await screen.findByRole('button', {name: 'Previous'})).toBeInTheDocument(); expect(screen.getByRole('button', {name: 'Next'})).toBeInTheDocument(); const gridCells = screen.getAllByTestId('grid-cell');