Skip to content

Instantly share code, notes, and snippets.

@kelsos
Created February 28, 2026 18:13
Show Gist options
  • Select an option

  • Save kelsos/5f5c2ca2748e41ea5c50020cccc7d732 to your computer and use it in GitHub Desktop.

Select an option

Save kelsos/5f5c2ca2748e41ea5c50020cccc7d732 to your computer and use it in GitHub Desktop.
Pinia Store Cleanup Plan — rotki frontend
# Pinia Store Cleanup Plan
Analysis of 47 Pinia stores (~7,500 LOC) in the rotki frontend identified several performance and design issues. This plan organizes fixes by effort and risk.
---
## Quick Wins (~30 min, low risk)
### 1. Fix `toRefs()` → `storeToRefs()` (7 files)
Using `toRefs()` on a Pinia store wraps **all** properties (including actions) as refs. `storeToRefs()` only extracts reactive state and getters.
| File | Line |
|------|------|
| `frontend/app/src/components/helper/OnboardingSettingsButton.vue` | 7 |
| `frontend/app/src/composables/defi/metadata.ts` | 14 |
| `frontend/app/src/composables/defi/airdrops/metadata.ts` | 14 |
| `frontend/app/src/composables/info/chains.ts` | 41 |
| `frontend/app/src/components/staking/kraken/KrakenStaking.vue` | 22 |
| `frontend/app/src/modules/history/events/use-history-events-status.ts` | 25–26 |
### 2. Fix notification store `nextId` — expensive sort just to find max
**File:** `store/notifications/index.ts:51-57`
```typescript
// Current — sorts entire array to find max
const nextId = computed<number>(() => {
const ids = get(data).map(value => value.id).sort((a, b) => b - a);
return ids.length > 0 ? ids[0] + 1 : 1;
});
// Fix — use Math.max or a running counter
const nextId = computed<number>(() => {
const ids = get(data).map(value => value.id);
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
});
```
### 3. Replace unnecessary watcher in statistics store
**File:** `store/statistics/index.ts:47-53`
```typescript
// Current — watcher syncing one ref to another
const scrambleMultiplier = ref<number>(get(scrambleMultiplierRef) ?? 1);
watchEffect(() => {
const newValue = get(scrambleMultiplierRef);
if (newValue !== undefined)
set(scrambleMultiplier, newValue);
});
// Fix — just use a computed
const scrambleMultiplier = computed<number>(() => get(scrambleMultiplierRef) ?? 1);
```
---
## Medium Effort
### 4. Fix monitor store reset leak + typing
**File:** `store/monitor.ts`
- Intervals stored in `Record<string, any>` are never cleared on `$reset()` — timers keep running after logout
- Add a `reset()` method that calls `stop()` so the reset plugin triggers cleanup
- Type `monitors` properly: `Record<string, ReturnType<typeof setInterval>>` instead of `any`
### 5. Fix notification store double-sort
**File:** `store/notifications/index.ts:44-46`
```typescript
// Current — sorts twice
const prioritized = computed<NotificationData[]>(() => {
const byDate = orderBy(get(data), ['date'], ['desc']);
return orderBy(byDate, [(n) => n.priority ?? Priority.NORMAL], ['desc']);
});
// Fix — single multi-key sort
const prioritized = computed<NotificationData[]>(() =>
orderBy(get(data), [(n) => n.priority ?? Priority.NORMAL, 'date'], ['desc', 'desc'])
);
```
### 6. Add reset cleanup to websocket + tasks stores
- **`store/websocket.ts`** — ensure WebSocket disconnect on `$reset()`
- **`store/tasks/index.ts`** — clear the non-reactive `handlers` registry and reset the plain `isRunning` boolean on `$reset()`
---
## Larger Refactors (discuss before starting)
### 7. Statistics store — composables created inside computed
**File:** `store/statistics/index.ts:82-141`
The `overall` computed creates new `useNumberScrambler` instances and new `computed()` refs **every time it re-evaluates**. Composables should be instantiated once during setup.
```typescript
// Problem — inside computed body
const overall = computed<Overall>(() => {
// ...
const scrambledNetWorth = get(useNumberScrambler({ // new instance each run!
value: computed(() => totalNW), // new computed each run!
}));
});
```
Fix: hoist `useNumberScrambler` calls to store setup scope; pass reactive inputs.
Similarly, `getNetValue()` (line 145) returns a new `computed` per call. When called from `overall` (line 89) it creates a new computed on every re-evaluation.
### 8. Addresses-names store — nested computed factories
**File:** `store/blockchain/accounts/addresses-names.ts:59-72`
`ensNameSelector()` and `getEnsAvatarUrl()` are factory functions that create new computed refs per call. `getEnsAvatarUrl` even nests them — it calls `ensNameSelector(address)` which creates a computed, then wraps that in another computed.
These are fine when called once per component setup, but audit callers to ensure they aren't invoked inside other computed properties or render loops.
### 9. Frontend settings store — 49 `useComputedRef` wrappers
**File:** `store/settings/frontend.ts:20-68`
Creates 49 individual `computed` refs from a single `settings` ref via `useComputedRef`. Each is just `computed(() => get(inp)[prop])`. Consider whether a single `reactive`/`toRefs` pattern would be cleaner.
**Note:** Benchmark before changing — the current approach may be fine in practice since each computed is lightweight. The `markRaw` on the settings object is intentional for performance.
---
## Suggested PR Grouping
| PR | Items | Risk | Description |
|----|-------|------|-------------|
| **PR 1** | 1, 2, 3 | Low | Quick wins: toRefs fix, nextId, watcher → computed |
| **PR 2** | 4, 5, 6 | Low–Medium | Store reset/cleanup + notification sort |
| **PR 3** | 7, 8 | Medium | Computed factory refactors (needs careful testing) |
| **PR 4** | 9 | Low | Settings store (optional, benchmark first) |
---
## Additional Notes
- All 47 stores use **setup syntax** (composition API) — no options API stores
- Error handling is generally consistent (try/catch → `notifyError` + logger)
- No circular store dependencies found
- API call patterns are inconsistent across stores (direct calls, item cache, task-based) but this is a larger architectural concern, not a quick fix
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment