Pinia shows up on most Vue job posts, so people add it to a new Laravel app on day one. Then the post list lives in two places and one of them is stale. Here is the line between page props and a store, and how to install Pinia on the starter kit without breaking SSR.
TL;DR
- Server data (posts, users, counts) stays in page props. A store that mirrors Eloquent goes stale.
- Pinia is for client state the server doesn’t own: a draft, an open palette, a wizard step.
- Install it inside the starter kit’s
withAppcallback so each server render gets its own store.
An Inertia page already receives its data as props. Copying the post list into a store gives you two sources of truth and a stale screen.
Use Pinia for state the server does not own.
What stays in props#
The CRUD guide loads posts in the controller and types them with defineProps. Search lives in the query string. After a save, the redirect renders the index again and the list is fresh.
That is the server state. Do not router.reload into a Pinia store “so every component can read it.” Pass the prop, or read a shared prop Inertia already keeps. A store that mirrors Eloquent will be wrong the moment someone else creates a row, unless you also build the Reverb path to patch it.
What belongs in a store#
A draft title the author typed on /posts/create, then lost by opening the list, is client state. So is a command palette that is open, or a multi-step wizard you have not turned into URLs yet.
| State | Where it lives |
|---|---|
| Posts, users, counts from the database | Page props |
| Search term, filters, current page | The query string |
| A draft that should survive leaving the page | Pinia |
| An open command palette or modal | Pinia (or a local ref) |
| Anything a refresh must remember | The URL or the database |
Tip: the refresh test
If a refresh should forget it, it can live in Pinia. If a refresh should remember it, put it in the URL or in the database.
Install it on the starter kit#
npm install piniaThe Vue starter kit mounts Inertia in resources/js/app.ts with withApp. Add Pinia inside that callback, and create the instance there, so a server render does not reuse another request’s state. Keep the title, layout, progress, and focus directive the file already has.
import { createPinia } from "pinia";
// inside the withApp callback resources/js/app.ts already has
withApp(app) {
app.use(createPinia());
},Gotcha: don't add a setup callback
A manual setup callback turns off the Vite plugin’s SSR wiring. Stay in withApp.
A draft store#
import { defineStore } from "pinia";
import { ref } from "vue";
export const useDraftStore = defineStore("draft", () => {
const title = ref("");
const excerpt = ref("");
function clear() {
title.value = "";
excerpt.value = "";
}
return { title, excerpt, clear };
});<script setup lang="ts">
import { useDraftStore } from "@/stores/draft";
const draft = useDraftStore();
</script>
<template>
<input v-model="draft.title" name="title" />
<textarea v-model="draft.excerpt" name="excerpt" />
</template>v-model is justified here because the store is the source. The <Form> component in the CRUD guide does not need it: the browser owns the inputs until submit. Use the store when leaving the page should keep the text.
Gotcha: clear the draft
Call draft.clear() after a successful create, or the next new post opens with the old sentence.
Watch the draft survive
Type a title on /posts/create, click to /posts, then come back. The text is still there. Refresh the page and it is gone, which is exactly the promise of client state.
Vue Router apps#
A separate SPA from the Sanctum guide has no Inertia props. Pinia is a normal place for the current user after GET /api/user, and for data you do not want to refetch on every route. Still treat the API as the source. Write the response into the store. Do not let the store be the only copy of a record you have not saved.
Vuex is the old answer. New Vue 3 code uses Pinia. The starter kit does not install either until you do.
Recap#
- Page props are your server state; don’t copy them into a store.
- Pinia holds client state the server doesn’t own and a refresh may forget.
- Install it inside
withAppso SSR requests don’t share a store. - In a Vue Router SPA, Pinia can cache API responses, but the API stays the source.
FAQ#
Do I need Pinia with Inertia?
Often not. Inertia passes server data as page props. Add Pinia when you have client-only state, like a draft or an open palette, that must survive a visit.
Where do I register Pinia on the Laravel Vue starter kit?
Inside the withApp(app) callback in resources/js/app.ts, with app.use(createPinia()).
Should I still use Vuex?
No. Vuex is the old answer; new Vue 3 code uses Pinia.
Next steps#
- Build a CRUD with Laravel, Inertia and Vue: server state in props.
- Server-render an Inertia app: why the store is created per request.
- Authenticate a Vue SPA with Sanctum: Pinia in a Vue Router app.
- Pinia docs



