# State Management with Pinia

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Routing and State
Source: https://www.devtutorials.ai/learn/vue/routing-state/state-management-with-pinia

> Local vs shared state and when to use a store, createPinia(), option stores and setup stores, state, getters and actions, async actions with errors, storeToRefs(), $patch, $reset and resetting setup stores, stores using stores, stores outside components, $subscribe, $onAction, persistence and Pinia plugins.

## Shared State

- **Local state** belongs to one component, such as whether a dropdown is open.
- **Shared state** is needed by many components far apart, such as the cart.
- Props and events get messy for shared state. You pass data through components that do not need it.
- A **store** = one place, outside any component, that holds shared state and the code that changes it.
- **Pinia** = the official store library for Vue.

**Simple lesson:** put state that many far-apart components need into a Pinia store.

### A Real-World Example

- Meera's menu has **Add** buttons. Her header shows the cart count.
- The two are far apart in the component tree.
- A cart store holds the items. Both components use it.

</script>

<template>
</template>
`,
    "SiteHeader.vue": `<script setup lang="ts">

const cart = useCartStore();
</script>

<template>
    <header>Meera's Kitchen · 🛒 {{ cart.count }}</header>
</template>
`,
    "MenuList.vue": `<script setup lang="ts">

const cart = useCartStore();
const dishes: string[] = ["Dosa", "Idli", "Vada"];
</script>

<template>
    <p v-for="d in dishes" :key="d">{{ d }} <button @click="cart.add(d)">Add</button></p>
</template>
`,
  }}
/>

- **What happened:** `MenuList` added items. `SiteHeader` showed the new count.
- No props or events were passed between them.

### What You Will Learn

1. When a store is the right tool.
2. How to install Pinia.
3. Option stores: state, getters and actions.
4. Setup stores, built like composables.
5. How to read a store safely with `storeToRefs()`.
6. How to change many values with `$patch()` and reset a store.
7. Async actions with loading and errors.
8. Stores that use other stores, and stores outside components.
9. `$subscribe`, `$onAction`, saving to `localStorage` and plugins.

## When to Use a Store

<div className="tscroll">

| State | Where it lives |
|---|---|
| Used by one component | A `ref` in that component |
| Shared by a parent and a few children | Props and events |
| Shared down one branch | `provide` / `inject` |
| Shared across the whole app (user, cart, settings) | A Pinia store |

</div>

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-vue-pinia" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-pinia">Animation: the cart store sits in the middle. MenuList calls the add action, the store's state changes, and SiteHeader and CartPage both update</title>
  <rect x="245" y="70" width="150" height="60" rx="14" style={{ fill: "var(--ngc-yellow)", stroke: "var(--ngc-orange)", strokeWidth: 2, fillOpacity: 0.25 }} />
  <text x="320" y="97" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700 }}>cart store</text>
  <text x="320" y="117" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--mono)", fontSize: 11 }}>items, count, add()</text>
  <g style={{ fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700 }}>
    <rect x="20" y="75" width="140" height="50" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} /><text x="90" y="105" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>MenuList</text>
    <rect x="480" y="20" width="140" height="50" rx="10" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }} /><text x="550" y="50" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}>SiteHeader</text>
    <rect x="480" y="130" width="140" height="50" rx="10" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }} /><text x="550" y="160" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}>CartPage</text>
  </g>
  <path d="M160 100 H243" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} />
  <path d="M395 90 L478 50 M395 110 L478 150" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, strokeDasharray: "5 4" }} />
  <circle r="7" cy="100" style={{ fill: "var(--ngc-blue)" }}><animate attributeName="cx" values="160;243;243" keyTimes="0;0.3;1" dur="5s" repeatCount="indefinite" /><animate attributeName="opacity" values="1;1;0;0" keyTimes="0;0.3;0.31;1" dur="5s" repeatCount="indefinite" /></circle>
  <circle r="7" opacity="0" style={{ fill: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.4;0.41;0.7;0.71" dur="5s" repeatCount="indefinite" /><animateMotion dur="5s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.4;0.7;1" path="M395 90 L478 50" /></circle>
  <circle r="7" opacity="0" style={{ fill: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.4;0.41;0.7;0.71" dur="5s" repeatCount="indefinite" /><animateMotion dur="5s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.4;0.7;1" path="M395 110 L478 150" /></circle>
</svg>
<figcaption>One component calls an action. Every component that reads the store updates.</figcaption>
</figure>

## Installing Pinia

- In a real project: `npm install pinia`. create-vue can add it for you.
- In `main.ts`: `app.use(createPinia())`. Pinia is a plugin.
- Put stores in a `stores/` folder, one file per store.
- Name the function `useXStore`, like a composable.

## Option Stores

- `defineStore("id", { state, getters, actions })` makes an **option store**.
- **state** = a function returning the starting data.
- **getters** = computed values from the state.
- **actions** = methods that change the state. They can be `async`.
- Inside getters and actions, use `this`, as in the **Options API**.

const order = useOrderStore();
</script>

<template>
    <p>Table {{ order.table }}: {{ order.plates }} plates, ₹{{ order.total }} {{ order.bigOrder ? "(big!)" : "" }}</p>
    <button @click="order.addPlate()">Add a plate</button>
    <button @click="order.$patch({ table: 7, plates: 3 })">$patch table 7, 3 plates</button>
    <button @click="order.$reset()">$reset</button>
</template>
`,
  }}
/>

- **What happened:** `$patch` changed two values in one step.
- `$reset()` put the starting state back. It works for option stores only.

**Simple lesson:** option stores have `state`, `getters` and `actions`.

Vue DevTools has a Pinia tab. It shows every store, its state and a timeline of actions, and lets you edit state while the app runs.

## Setup Stores

- `defineStore("id", () => { ... })` makes a **setup store**.
- It is written like a composable: `ref` = state, `computed` = getters, functions = actions.
- Return **every** piece of state. State you keep private is missed by DevTools, SSR and plugins.
- It can use watchers and other composables.
- `$reset()` does not exist here. Write your own `reset()` action.

### Reading a Store Safely

- The store is a `reactive` object. Destructuring it breaks reactivity, as you learned in **Reactive State**.
- `storeToRefs(store)` turns state and getters into refs that stay linked.
- Take actions straight from the store. They are plain functions.

const booking = useBookingStore();
const { guests, label } = storeToRefs(booking);
const { addGuest, reset } = booking;
</script>

<template>
    <p>{{ label }} (guests ref: {{ guests }})</p>
    <button @click="addGuest">Add guest</button>
    <button @click="booking.time = '21:00'">Later</button>
    <button @click="reset">Reset</button>
</template>
`,
  }}
/>

`const { guests } = useBookingStore()` gives a frozen copy. Always destructure state and getters with `storeToRefs()`. Actions can be destructured directly.

**Simple lesson:** setup stores are composables with a name. Read them with `storeToRefs()`.

## Async Actions

- Actions can be `async`. Use them to load and save data.
- Keep `loading` and `error` in the store, so every component can show them.
- Catch errors in the action, or let the caller handle them.

const menu = useMenuStore();
const { dishes, loading, error } = storeToRefs(menu);
</script>

<template>
    <button @click="menu.load()">Load menu</button>
    <button @click="menu.load('https://api.meeraskitchen.dev/error')">Load (server error)</button>
    <p v-if="loading">Loading…</p>
    <p v-else-if="error">Error: {{ error }}</p>
    <ul v-else>
      <li v-for="d in dishes" :key="d.id">{{ d.name }}: ₹{{ d.price }}</li>
    </ul>
</template>
`,
  }}
/>

## Stores Together

- **Store in a store:** call `useCartStore()` inside another store's action or getter.
- **Composables in a store:** setup stores can call composables such as `useFetch`.
- **Outside components:** call `useXStore()` in any function that runs **after** `app.use(pinia)`, such as a router guard.

const cart = useCartStore();
const checkout = useCheckoutStore();
</script>

<template>
    <p>Cart: {{ cart.items.join(", ") || "empty" }}</p>
    <button @click="cart.items.push('Idli')">Add idli</button>
    <button @click="checkout.placeOrder()">Place order</button>
    <p>Plates sent to the kitchen: {{ checkout.placed }}</p>
</template>
`,
  }}
/>

## Watching and Saving a Store

- `store.$subscribe((mutation, state) => ...)` runs after every state change.
- `store.$onAction(({ name, args, after, onError }) => ...)` runs when an action is called.
- To keep the cart after a reload, save it to `localStorage` in `$subscribe`, and read it back when the store starts.
- A **Pinia plugin** (`pinia.use(fn)`) runs for every store. It is the usual place for saving.
- Real projects often install `pinia-plugin-persistedstate` and add `persist: true` to a store instead of writing this by hand.

const likes = useLikesStore();

likes.$subscribe((mutation, state) => {
    console.log("state changed, count =", state.count, "saved:", localStorage.getItem("pinia:likes"));
});

likes.$onAction(({ name, after }) => {
    console.log("action called:", name);
    after(() => console.log("action finished:", name));
});
</script>

<template>
    <button @click="likes.like()">❤ {{ likes.count }}</button>
</template>
`,
  }}
/>

Real apps often use a ready plugin such as `pinia-plugin-persistedstate`. In this lab, each run starts with an empty in-memory `localStorage`, so saved data lasts until you press Run again.

**Simple lesson:** `$subscribe` sees changes, `$onAction` sees actions, plugins add features to every store.

## TL;DR

- **Store** = shared state and its logic, outside components.
- **`createPinia()`** = install with `app.use(createPinia())`.
- **`defineStore("id", {...})`** = option store: `state`, `getters`, `actions`.
- **`defineStore("id", () => {...})`** = setup store: refs, computed, functions.
- **`storeToRefs(store)`** = safe destructuring of state and getters.
- **`$patch()`** = change many values at once.
- **`$reset()`** = option stores only. Write `reset()` in setup stores.
- **`$subscribe` / `$onAction`** = after state changes / when actions run.
- **`pinia.use(plugin)`** = adds features, such as saving, to every store.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Destructuring the store directly | `const { count } = store` stays live | Use `storeToRefs(store)` | The value freezes |
| Everything in a store | Stores are always better | Keep local state local | Hard-to-follow global state |
| `$reset()` on a setup store | Every store has it | Write a `reset()` action | An error |
| Calling a store before `app.use(pinia)` | Stores work anywhere | Call it after Pinia is installed | "getActivePinia was called with no active Pinia" |
| Changing state from far-away components | Any component may edit freely | Change state through actions | Hard to find who changed what |
| Saving the whole state on every keystroke to a server | `$subscribe` is free | Debounce, or save in actions | Too many requests |

</div>

## Check Yourself

## Exercises

## What to Remember

- Use a store for app-wide shared state. Keep local state local.
- Option stores: `state`, `getters`, `actions`. Setup stores: refs, computed, functions.
- Destructure state with `storeToRefs()`. Actions can be destructured directly.
- `$patch` changes many values. `$reset` is for option stores only.
- `$subscribe`, `$onAction` and plugins watch, log and save stores.

Next: **HTTP Requests**
