---
title: "Pinia storeToRefs — Array Reassignment Breaks Reactivity"
aliases: [pinia-storeftorefs-reactivity, pinia-array-assignment, pinia-stale-ref]
tags: [pinia, vue, javascript, reactivity, frontend, gotcha, debugging]
sources:
  - "daily/2026-05-06.md"
created: 2026-05-06
updated: 2026-05-06
---

# Pinia `storeToRefs` — Array Reassignment Breaks Reactivity

In Pinia Options API stores, replacing an array via direct assignment (`this.timeline = newArray`) makes the `Ref` returned by `storeToRefs` go stale. The component's destructured ref holds a reference to the old array object — it never sees the new array. The store itself has correct data; the component sees `[]`.

## Key Takeaways

- `this.timeline = data` in a Pinia Options API action causes `storeToRefs`-derived refs to be orphaned — `timeline.value` stays empty while `reportsStore.timeline` has 7 items
- The stale ref fires `watchEffect` exactly once (with the initial empty state) and never again — confirming the ref is orphaned, not the store
- Fix: mutate in-place with `this.timeline.splice(0, this.timeline.length, ...data)` OR use `this.$patch({ timeline: data })`
- Audit all Pinia Options API stores for `this.x = newArray` patterns — replace with splice or $patch
- Related: Vue auth race condition — `initAuth()` called in `App.vue onMounted` races with child component `onMounted` fetches; fix: call `initAuth()` synchronously in `main.ts` before `app.mount()`

## Details

### The Stale Ref Pattern

```javascript
// Pinia Options API store — BROKEN
export const useReportsStore = defineStore('reports', {
  state: () => ({
    timeline: [],
  }),
  actions: {
    async fetchTimeline(from, to) {
      const data = await reportsAPI.getTimeline(from, to);
      this.timeline = data;  // ← BREAKS storeToRefs reactivity
    },
  },
});
```

```vue
<!-- Component — storeToRefs ref becomes stale -->
<script setup>
const reportsStore = useReportsStore();
const { timeline } = storeToRefs(reportsStore);  // captures ref to original array

await reportsStore.fetchTimeline(from, to);
// reportsStore.timeline.length === 7  ✓ store has data
// timeline.value.length === 0         ✗ ref is stale — points to old []
</script>
```

### The Fix

```javascript
// Option A: Mutate in-place (preferred — no API change)
async fetchTimeline(from, to) {
  const data = await reportsAPI.getTimeline(from, to);
  this.timeline.splice(0, this.timeline.length, ...data);  // mutates, ref stays valid
},

// Option B: $patch (explicit, works in both Options and Setup API)
async fetchTimeline(from, to) {
  const data = await reportsAPI.getTimeline(from, to);
  this.$patch({ timeline: data });
},
```

### Diagnosis Signals

When `data.value.length === 0` but `store.data.length > 0` after an `await`:

1. **`watchEffect` fires once then stops** — stale ref, not reactive to store changes
2. **Identity check** — `storeToRefs` returns a ref to the original array object; assigning a new array breaks the link
3. **Direct store access works** — `{{ reportsStore.timeline }}` in template renders correctly; `{{ timeline }}` does not

### Auth Initialization Race Condition

A related Vue bootstrap gotcha: if `initAuth()` (restoring token from `localStorage`) is called in `App.vue`'s `onMounted`, it races with child components that also call `onMounted` and fire API requests. Both `onMounted` hooks run in the same microtask queue — child components may fetch before auth is set.

```javascript
// ❌ BROKEN — App.vue onMounted races with child component onMounted
// App.vue
onMounted(() => {
  authStore.initAuth();  // may run AFTER DashboardView's onMounted
});

// DashboardView.vue
onMounted(async () => {
  await fetchData();  // fires with token=null if initAuth lost the race
});
```

```javascript
// ✅ CORRECT — main.ts, synchronous before app.mount()
const app = createApp(App);
app.use(pinia);
app.use(router);

// Initialize auth synchronously BEFORE mount
const authStore = useAuthStore(pinia);
authStore.initAuth();  // reads from localStorage synchronously

app.mount('#app');  // all components now have token on first render
```

## Related Concepts

- [[wiki/concepts/zustand-async-hydration]] — same class of "state not ready on first render" bug in React/Zustand
- [[wiki/concepts/react-useref-event-handler-state]] — React's async `useState` requiring `useRef` for synchronous access
- [[wiki/concepts/websocket-react-token-guard]] — similar auth-before-connect pattern

## Sources

- [[daily/2026-05-06.md]] — cc-dashboard: Dashboard charts empty despite API returning data; `reportsStore.timeline.length === 7` but `timeline.value.length === 0` after await; fixed by replacing `this.timeline = data` with `this.timeline.splice(...)`; auth race condition fixed by moving `initAuth()` to `main.ts` before `app.mount()`
