apache / apache/superset

[SIP-207] Replace Redux with Zustand and TanStack Query

Open
#39,209 11 comments 2 reactions 0 assignees View on GitHub
design:proposal frontend:refactor sip
Dominant language
Python
Stars
74.8k
Forks
18.3k
Avg merge
2d 5h
Merged PRs (30d)
685

Description

## [SIP-207] Replace Redux with Zustand and TanStack Query

This proposal introduces a clear separation between two fundamentally different kinds of state that are currently handled by the same tool: **UI state** (edit mode, active tabs, modal visibility; things that live only in the browser) and **server state** (charts, dashboards, datasets; things that originate from the API and need caching and invalidation). We propose adopting [Zustand](https://github.com/pmndrs/zustand) for UI state and [TanStack Query](https://github.com/tanstack/query) for server state, retiring Redux.

A first full implementation of this proposal is underway: PR [#41548](https://github.com/apache/superset/pull/41548) migrates the **Dashboard** feature (Phase 3 below) off Redux to [Zustand](https://github.com/pmndrs/zustand) for UI state and [TanStack Query](https://github.com/tanstack/query) for server state, exercising the full pattern end-to-end. An earlier proof-of-concept by [@mistercrunch](https://github.com/mistercrunch) (PR [#34917] https://github.com/apache/superset/pull/34917), now closed) separately validated [TanStack Query](https://github.com/tanstack/query) against the Superset Charts API; that POC relied on Orval code generation, which this proposal defers (see Rejected Alternatives).

This SIP sits slightly ahead on the roadmap relative to the active Extensions architecture effort. The Extensions work is the nearer-term priority, focused on adding real value to extension authors quickly. This migration is about improving foundational state management, and the two efforts complement each other: as the Extensions architecture matures, having [Zustand](https://github.com/pmndrs/zustand) instead of Redux will make the integration points cleaner and the extension authoring experience easier to build on.

### Motivation

The Superset Redux store (`src/views/store.ts`) registers 20 reducers as a single global object. Twelve of those reducers are scoped to a single feature (SQL Lab, Explore, the Dashboard page, or the database selector) but forced into global state regardless. The only state that genuinely crosses feature boundaries is `user`, `common`, `charts`, `datasources`, and `messageToasts`.

Every Redux slice has one of three exits: **migrate** (move to a [Zustand](https://github.com/pmndrs/zustand) store or [TanStack Query](https://tanstack.com/query)), **defer** (cross-app; stays Redux until the last consumer migrates), or **delete** (legacy / superseded; remove instead of porting).

| Reducer | Actually used by |
|---|---|
| `sqlLab` | SQL Lab only |
| `saveModal` | Explore's save dialog only |
| `reports` | Report modal only |
| `dashboardInfo` | Dashboard page only |
| `dashboardState` | Dashboard page only |
| `dashboardFilters` | Dashboard page only |
| `dashboardLayout` | Dashboard page only |
| `nativeFilters` | Dashboard page only |
| `sliceEntities` | Dashboard page only |
| `dataMask` | Dashboard page only |
| `explore` | Chart builder only |
| `database` | Database selector only |

One slice in the table above does not follow the routine "migrate" path: `dashboardFilters` holds state for the legacy filter-box system. Its exit (migrate or delete) follows the separate decision on filter-box deprecation. Until that decision lands, it remains Redux and is **not** part of Phase 3.

Cross-feature state, called out separately because the exit depends on the last consumer:

| Slice | Exit |
|---|---|
| `user`, `common`, `impressionId` | Migrate to a slim global [Zustand](https://github.com/pmndrs/zustand) store (Phase 5) |
| `messageToasts` | Migrate to a standalone [Zustand](https://github.com/pmndrs/zustand) store (Phase 5) |
| `charts` | Defer until `Explore` migrates; then split between [TanStack Query](https://tanstack.com/query) (results) and feature stores (coordination state) |
| `datasources` | Defer until `Explore` migrates; then move to [TanStack Query](https://tanstack.com/query) |

This conflation has real costs. Adding a single boolean to dashboard state today requires touching five files: an action type constant, an action creator, a reducer case, a type update, and `useSelector`/`useDispatch` calls in every consumer. A comment in `store.ts` documents the resulting pressure:

```ts
// TODO: This reducer is a combination of the Dashboard and Explore reducers.
// The correct way of handling this is to unify the actions and reducers from both
// modules in shared files. This involves a big refactor to unify the parameter types
// and move files around. We should tackle this in a specific PR.
const CombinedDatasourceReducers = (datasources, action) => { ... }
```

There is also a bundle cost. Twelve of the twenty reducers are scoped to a single route but all twelve are registered in `src/views/store.ts` and loaded unconditionally in the main bundle. A user visiting only the list views pays for the SQL Lab and dashboard state machinery they will never use in that session. Feature-scoped [Zustand](https://github.com/pmndrs/zustand) stores load only when the route that owns them is visited.

Server data has the same problem from a different angle. Three fetching patterns coexist with no documented rule for which to use:

- **RTK Query**: proper caching with tag-based invalidation, but scoped to a handful of SQL Lab endpoints and unable to be adopted broadly without deepening Redux coupling
- **`cachedSupersetGet`**: a manual `Map-based` cache with no TTL, used throughout dashboard filters and dataset selectors
- **Raw `SupersetClient` calls**: the dominant pattern; direct HTTP calls with no caching, no deduplication, no shared loading state

A comment in `apiResources.ts` captures the situation:
`// TODO Store the state in redux or something, share state between hook instances`

The result is that two components requesting the same endpoint fire two independent HTTP requests, and a list view re-mounted after navigation re-fetches from scratch every time.

The server state problem has a direct performance cost: parallel component mounts against the same endpoint each fire their own network request, and navigating away from a list view and back triggers a full re-fetch with no cached data to show in the meantime.

### Proposed Change

The proposal draws a clear boundary between two categories of state and assigns the right tool to each:

```
┌──────────────────────────────┬──────────────────────────────┐
│ UI / Client State │ Server / Data State │
├──────────────────────────────┼──────────────────────────────┤
│ • Edit mode │ • Charts list │
│ • Active tab │ • Dashboard metadata │
│ • Modal open/closed │ • Dataset columns │
│ • Filter panel state │ • Query results │
├──────────────────────────────┼──────────────────────────────┤
│ Zustand │ TanStack Query │
│ │ │
│ Global store │ │
│ (user, common) │ │
│ │ │
│ Feature stores │ │
│ (dashboard, sqlLab, ...) │ │
└──────────────────────────────┴──────────────────────────────┘

React Context: dependency injection only (router, locale, app-level config)
```

#### Decision guide

| Question | Tool |
|---|---|
| Did this data come from the API? | TanStack Query |
| Is this UI state shared across multiple components? | Zustand |
| Is this state used only inside one component? | `useState` / `useReducer` |
| Are you passing configuration down the tree (router, locale, app-level config)? | React Context |

The rules in plain language:
1. If it came from the server, it belongs in [TanStack Query](https://tanstack.com/query). Server data has different needs: it goes stale, it needs invalidation after mutations, and multiple components often request the same endpoint. [Zustand](https://github.com/pmndrs/zustand) is not a cache.
2. If multiple components need the same UI state, it belongs in a [Zustand](https://github.com/pmndrs/zustand) store. Context is not a state manager: it re-renders every consumer on every change.
3. If only one component uses it, `useState` is the right call. Not everything needs a store.
4. React Context is for dependency injection, not state. Theme tokens, router instances, and locale config belong there because they rarely change.

#### Zustand for UI state

[Zustand](https://zustand.docs.pmnd.rs) replaces Redux reducers with lightweight stores. There is no action type registry, no action creator factory, no reducer pattern, and no `dispatch`.

**Before (Redux):**
```ts
// dashboard/actions/dashboardState.ts
export const SET_EDIT_MODE = 'SET_EDIT_MODE';
interface SetEditModeAction { type: typeof SET_EDIT_MODE; editMode: boolean }
export function setEditMode(editMode: boolean): SetEditModeAction {
return { type: SET_EDIT_MODE, editMode };
}
// ...plus reducer case, useSelector + useDispatch in every consumer
```

**After (Zustand):**
```ts
export const useDashboardStore = create()(
devtools(set => ({
editMode: false,
setEditMode: editMode => set({ editMode }, false, 'dashboard/setEditMode'),
}))
);
// In any consumer: no Provider, no dispatch, no selector boilerplate
const editMode = useDashboardStore(s => s.editMode);
const setEditMode = useDashboardStore(s => s.setEditMode);
```

Stores are organized at two levels: a slim global store (built in Phase 5 once cross-feature consumers migrate) for state genuinely consumed across unrelated features (`user session`, `common config`), built using [Zustand](https://github.com/pmndrs/zustand)'s slices pattern so concerns remain separated. Feature-local stores live co-located with the components that own them, added incrementally as each module migrates (e.g. `src/dashboard/stores/`, `src/SqlLab/stores/`).

**Within a feature, split into multiple stores rather than one.** Two pieces of state belong in the same store only if they need to update atomically; everything else gets its own store. This is the [Zustand](https://github.com/pmndrs/zustand) maintainer's guidance and the rule this migration follows. A monolithic feature store would replicate the structural problem of today's Redux store at smaller scale.

[Zustand](https://github.com/pmndrs/zustand) stores are significantly easier to test. Because a store is just a function, it can be exercised as a plain unit test with no React, no Provider, and no mock store setup:

```ts
test('setEditMode updates state', () => {
useDashboardStore.getState().setEditMode(true);
expect(useDashboardStore.getState().editMode).toBe(true);
});
```

Contrast this with the current Redux pattern, which requires `configureStore` with preloaded state, a `` wrapper, and assertions against dispatched actions rather than state directly. [Zustand](https://github.com/pmndrs/zustand) also resets cleanly between tests via `store.setState(store.getInitialState(), true)`, removing the shared-state pollution that makes Redux test suites order-dependent.

[Zustand](https://github.com/pmndrs/zustand)'s `devtools` middleware connects to the Redux DevTools Extension, the same browser tool developers use today. Time-travel debugging and named action logging work the same way.

[Zustand](https://github.com/pmndrs/zustand) is also the durable foundation for the public Extension API. **Any extension API built after this SIP uses Zustand `subscribe` and [TanStack Query](https://tanstack.com/query) `useQuery` as its primitives, never Redux dispatch.** This is a hard architectural commitment, not a soft preference.

The benefit is measurable. A reactive event on Redux requires ~15-20 LoC each: action-listener middleware plus parallel stable-identity workarounds for Redux's object-identity churn (the kind that forces SQL Lab to maintain `getActiveEditorImmutableId` and `findQueryEditor`). On [Zustand](https://github.com/pmndrs/zustand) the equivalent reactive event is ~5 LoC via `subscribeWithSelector`. For a non-trivial public surface this compounds to roughly half the implementation size and removes a whole class of workaround abstractions. Extension authors also get cleaner hooks: subscriptions carry no dispatch protocol, no action-type knowledge, and no dependency on internal store shape.

Redux and [Zustand](https://github.com/pmndrs/zustand) are fully independent runtimes. A component can read from both simultaneously during the migration period with no Provider conflict and no integration layer. The recommended pattern is to migrate one feature store at a time.

Dashboard undo/redo (currently `redux-undo`) is replaced by [`zundo`](https://github.com/charkour/zundo), a [Zustand](https://github.com/pmndrs/zustand) `temporal` middleware. Because the dashboard store contains only dashboard state, non-layout updates never reach it, making the action-filtering workaround currently required by `redux-undo` unnecessary.

#### TanStack Query for server state

[TanStack Query](https://tanstack.com/query) provides automatic caching, request deduplication, stale-while-revalidate, and mutation-triggered cache invalidation. It replaces all three current fetching patterns with one approach.

**Before (raw SupersetClient, the dominant pattern):**
```ts
// dashboard/components/PropertiesModal/index.tsx
const [dashboard, setDashboard] = useState(null);
useEffect(() => {
SupersetClient.get({ endpoint: `/api/v1/dashboard/${id}` })
.then(({ json }) => setDashboard(json.result));
}, [id]);
// re-fetches on every mount; no shared cache across components
```

**After (TanStack Query):**
```ts
// defined once in src/features/dashboard/queries/useDashboard.ts
export function useDashboard(id: number) {
return useQuery({
queryKey: ['dashboard', id],
queryFn: () => SupersetClient.get({ endpoint: `/api/v1/dashboard/${id}` }).then(r => r.json.result),
});
}
// shared by every consumer: one request regardless of how many components call it
const { data: dashboard, isLoading, error } = useDashboard(id);
```

Users will see content immediately when returning to a list view: cached data renders on navigation while a background refetch runs, replacing the current full loading state. Components mounting simultaneously against the same query key share one in-flight request rather than each firing their own. Configuring `staleTime` per query type extends these cache benefits to sequential mounts, serving data from cache for an appropriate window and reducing redundant network traffic across the app.

[TanStack Query](https://tanstack.com/query) requires no Redux. The natural alternative was RTK Query, which is already partially in use for SQL Lab, but its cache lives as a Redux slice that requires store middleware registration, meaning broad adoption would deepen the coupling this migration aims to remove, not reduce it. [TanStack Query](https://tanstack.com/query) also supports infinite queries and offline mutation queuing, which RTK Query does not.

#### React Context

React Context is not replaced. It retains its role as a dependency injection tool, passing theme tokens, router instances, and locale config down the tree. Existing Context-based state managers (e.g., `AutoRefreshContext`) are migrated to [Zustand](https://github.com/pmndrs/zustand) stores as each feature is touched.

#### Implementation Plan

The migration is incremental. Redux and [Zustand](https://github.com/pmndrs/zustand)/[TanStack Query](https://tanstack.com/query) coexist throughout, with no flag day and no contributor blocked.

**Phase numbers identify scope, not execution order.** Each phase describes what it delivers, not when it must happen. Teams may sequence phases based on value, contributor availability, or downstream unblocks (for example, prioritizing the feature whose migration unblocks the next public Extension API), provided the coexistence rules in *Migration Plan and Compatibility* are honored.

**Phase 0: Foundation.** Load-bearing for every later phase. It delivers:
- `@tanstack/react-query`, `zustand`, and `zundo` dependencies installed.
- `QueryClientProvider` mounted in the app root, with **explicit global defaults** for `staleTime` and `gcTime` (TanStack's defaults of `0` cause refetches on every remount, which is the wrong default for Superset's mostly-stable list and detail data; per-query overrides remain available).
- A **hierarchical query-key convention**: every feature's `queries/keys.ts` exports a builder with `all`, `lists()`, `list(filters)`, `details()`, `detail(id)`, and per-resource extensions, so invalidations can target the right scope without string drift.
- Redux DevTools wiring for [Zustand](https://github.com/pmndrs/zustand)'s `devtools` middleware, named action logging per store.
- The `oxlint` `no-restricted-imports` gate framework. Gates are activated progressively in `oxlint.json` as each phase completes.
- Migration conventions written into `CONTRIBUTING.md`.

The slim global store for `user` / `common` / `messageToasts` / `impressionId` is **not** built in Phase 0; it appears in Phase 5 when its first consumers migrate. Creating it earlier risks building a Redux-shaped god store ahead of any real need.

**Phase 1: Server state for list views.** Migrate `ChartList`, `DashboardList`, `DatasetList`, and `SavedQueryList` to [TanStack Query](https://tanstack.com/query) hooks, already validated by PR [#34917](https://github.com/apache/superset/pull/34917). Replace all `cachedSupersetGet` usages with [TanStack Query](https://tanstack.com/query) hooks. The database module's query execution and SQL formatting, which are entirely API-derived state, also migrate to [TanStack Query](https://tanstack.com/query) `useMutation` hooks in this phase.

**Phase 2: Module-local Redux slices.** Migrate `saveModal`, `reports` and `explore` slices to [Zustand](https://github.com/pmndrs/zustand) feature stores. These have no cross-feature consumers and are the lowest-risk targets.

**Phase 3: Dashboard.** The dashboard has the most Redux state of any feature. Slices with cross-slice read dependencies (`nativeFilters` and `dataMask`) are migrated together to avoid a permanent bridge layer. `dashboardFilters` is excluded; see "Slice exits" above. Once Explore has migrated, datasource fetching moves to [TanStack Query](https://tanstack.com/query), which eliminates the `CombinedDatasourceReducers` proxy in `store.ts`; if Phase 3 ships before Phase 2, the proxy is removed when Phase 2 lands. The `charts` reducer, which is consumed by both Dashboard and Explore, remains in Redux through this phase; filter computation logic that reads across both stores continues to work via the coexisting Redux store.

**Phase 4: SQL Lab.** Migrate SQL Lab actions and reducers to `useSqlLabStore`. The existing `persistSqlLabStateEnhancer` handles `FeatureFlag.SqllabBackendPersistence` branching, sensitive field filtering, and localStorage usage tracking; it is replaced by a custom [Zustand](https://github.com/pmndrs/zustand) `persist` storage adapter that preserves the same behavior. Existing RTK Query endpoints in `src/hooks/apiResources/` are migrated to [TanStack Query](https://tanstack.com/query). The `@apache-superset/core` extension API exposes public event hooks (`onDidQueryRun`, `onDidQuerySuccess`, and others) built on Redux action listeners; these are rebuilt on [Zustand](https://github.com/pmndrs/zustand) `subscribe()` calls before the Redux SQL Lab actions are removed, and a migration guide is published for extension authors.

**Phase 5: Redux retirement.** The remaining cross-feature reducers (`user`, `common`, `impressionId`) move to the slim global [Zustand](https://github.com/pmndrs/zustand) store, built in this phase. `messageToasts` moves to a standalone [Zustand](https://github.com/pmndrs/zustand) store co-located with its component rather than a global slice, since toast state is a fire-and-forget service with no coordination dependency on user or common state. The `charts` reducer holds both query response data and coordination state; these are split between [TanStack Query](https://tanstack.com/query) and the relevant feature stores respectively. The `loggerMiddleware`, which reads cross-feature state to enrich log events, migrates naturally as each feature store it depends on moves to [Zustand](https://github.com/pmndrs/zustand).

**External-package audit (blocker for `package.json` removal).** Redux can only be removed from `package.json` after Redux imports outside the host app are also migrated: `superset-frontend/packages/`, `superset-frontend/plugins/`, the embedded SDK, and any first-party overrides repositories. The audit happens in this phase. Any external Redux import discovered late is the single thing that will stall Redux removal.

### New or Changed Public Interfaces

All changes are frontend-only. No backend models, REST endpoints, or database schema are affected.

Removed as each phase completes: `src/explore/actions/` and `src/explore/reducers/` (Phase 2), `src/dashboard/actions/` and `src/dashboard/reducers/` (Phase 3), `src/SqlLab/actions/` and `src/SqlLab/reducers/` (Phase 4), `src/views/store.ts` (Phase 5).

Added: `src/store/` (global [Zustand](https://github.com/pmndrs/zustand) store with slices), `src/dashboard/stores/`, `src/SqlLab/stores/`, `src/explore/stores/` (feature-level [Zustand](https://github.com/pmndrs/zustand) stores), `src/dashboard/queries/`, `src/SqlLab/queries/`, `src/explore/queries/` ([TanStack Query](https://tanstack.com/query) hooks).

The `@superset-ui/embedded-sdk` has no Redux dependency and is unaffected. The `@apache-superset/core/sqlLab` package exposes a public event API built on Redux action listeners; it will be rebuilt on [Zustand](https://github.com/pmndrs/zustand) subscriptions as part of Phase 4 and a migration guide published before any breaking removal. All other Redux action creator and selector exports removed in each phase will be noted in `UPDATING.md`.

### New dependencies

| Package | Version | Purpose | License |
|---|---|---|---|
| `zustand` | ^5.x | UI/client state stores | MIT |
| `@tanstack/react-query` | ^5.x | Server state management | MIT |
| `@tanstack/react-query-devtools` | ^5.x | Development tooling | MIT |
| `zundo` | ^2.x | Undo/redo middleware for Zustand | MIT |

Removed as migration completes: `redux`, `react-redux`, `@reduxjs/toolkit`, `redux-thunk`, `redux-undo`, `redux-localstorage`.

During the coexistence window (Phases 0-4), both Redux and the new libraries are present. The net bundle reduction is realized at Phase 5 when Redux packages are removed.

### Migration Plan and Compatibility

No database migrations are required. The migration is frontend-only and additive at each phase. Existing Redux code continues to work until it is explicitly replaced.

From `Phase 0` forward, new state management work goes to [Zustand](https://github.com/pmndrs/zustand) and [TanStack Query](https://tanstack.com/query). No new Redux slices are added after this SIP is accepted. Existing action creators on migrating slices may be modified to dual-write during their coexistence window; see the patterns below.

**Coexistence patterns each phase will use:**
1. **Dual-write action creators.** During a slice's migration, its existing action creators write to both Redux and the new [Zustand](https://github.com/pmndrs/zustand) store until every consumer is on the new store. This is intentional `back-compat` scaffolding, not duplication.
2. **Dual-read selectors.** Mirror the above on the read side: components migrate one at a time, and during the window some still read Redux while others read [Zustand](https://github.com/pmndrs/zustand).
3. **Layer-2 cleanup.** Each phase ends with a cleanup pass that collapses the dual-write/dual-read scaffolding once no consumer needs the Redux side. The phase is not "done" until this pass lands.
4. **Reuse existing thunks rather than fork them.** When a migrated slice's mutation still needs data from a non-migrated slice, the new code dispatches the existing Redux thunk rather than reimplementing it. The thunk migrates naturally when its remaining Redux reads do. Forking is the worst of both worlds and reopens bugs already fixed in the original.

The decision guide above is enforced via oxlint's `no-restricted-imports` rule, which Superset already uses. Gates are activated progressively in `oxlint.json` as each phase completes:

- **Phase 0**: `cachedSupersetGet` and direct `src/views/store` imports are banned globally, replaced by [TanStack Query](https://tanstack.com/query) hooks.
- **Phases 2-4**: `useSelector`, `useDispatch`, and imports from the migrated module's `actions/` and `reducers/` directories are banned per module, with error messages pointing to the replacement store.
- **Phase 5**: `react-redux`, `@reduxjs/toolkit`, and `redux` are banned across the entire codebase.

This means the rules are not just documented, they are enforced at CI. A developer who reaches for Redux in a migrated module gets a lint error with a message pointing to the right pattern.

### Rejected Alternatives

**Keep Redux for UI state and adopt [TanStack Query](https://tanstack.com/query) for server state as a permanent arrangement.** This is a legitimate approach that delivers the most immediate value. [TanStack Query](https://tanstack.com/query) eliminates duplicate requests and inconsistent fetching patterns without touching the Redux UI state layer. The reason this proposal does not stop there: the UI state problems remain unsolved. Twelve route-scoped reducers continue to load unconditionally in the main bundle. The five-file boilerplate cost continues to push state out of the store and into local component state, which is the same pattern that produced the duplicate request problem in the first place. The `CombinedDatasourceReducers` technical debt has no resolution path. The extensions architecture requires abstracting state management away from extension authors, which is cleaner with [Zustand](https://github.com/pmndrs/zustand)'s API than with Redux's dispatch protocol. [TanStack Query](https://tanstack.com/query) alone is a significant and independently deployable improvement; this proposal argues that completing the Redux migration is worth the additional investment to resolve these costs permanently rather than carry them indefinitely.

**Keep Redux and extend RTK Query for server state.** RTK Query is a genuine improvement over manual calls and is already partially adopted. However, its cache lives as a Redux slice, so broad adoption deepens Redux coupling rather than reducing it. It also does not address the UI-state boilerplate problem. [TanStack Query](https://tanstack.com/query) is standalone and solves both.

**Replace Redux with React Context for module-local state.** Context re-renders all consumers on every value change and requires manual memoization to avoid performance issues. The boilerplate is comparable to Redux. [Zustand](https://github.com/pmndrs/zustand) avoids both problems through selector-based subscriptions with no Provider required.

**Use a single global [Zustand](https://github.com/pmndrs/zustand) store.** This would replicate the structural problem of the current Redux store, putting all state in one place regardless of scope. Feature stores are the default; the global store is the exception.

**Include Orval code generation.** PR [#34917](https://github.com/apache/superset/pull/34917) demonstrated Orval generating [TanStack Query](https://tanstack.com/query) hooks from Superset's OpenAPI spec. The concept is sound, but the spec currently requires a normalization layer for Flask-AppBuilder's Union type patterns, has documented accuracy issues (issue [#33884](https://github.com/apache/superset/issues/33884)), and has no CI validation. Orval is recommended as a follow-on once the spec is reliable. Hand-written [TanStack Query](https://tanstack.com/query) hooks follow the same pattern Orval would generate.
---

## Reference

- Proof-of-concept
- (TanStack Query + Superset Charts API): [PR #34917](https://github.com/apache/superset/pull/34917)
- Phase 3 (Dashboard) implementation: [PR #41548](https://github.com/apache/superset/pull/41548) (draft)
- Zustand: https://github.com/pmndrs/zustand
- TanStack Query: https://tanstack.com/query
- TanStack Query vs RTK Query: https://tanstack.com/query/latest/docs/framework/react/comparison
- zundo: https://github.com/charkour/zundo
- Orval (follow-on candidate): https://orval.dev

Contributor guide

Open the contributing guide

Research direction

Start with src/views/store.ts and PR #41548, which demonstrates the Dashboard migration described here. Compare the Redux slices listed in the proposal with their planned migrate, defer, or delete paths, then follow the relevant phase. Done means the applicable state uses the proposed Zustand and TanStack Query separation while deferred slices remain explicitly accounted for.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.