This episode covers managing global state with Pinia in Nuxt: defining stores with setup and options syntax, Pinia plugins, shared state patterns with composables, and how to synchronize state during SSR hydration.

As an application grows, many components need the same data — for example, the shopping cart contents shown in the header, the cart page, and the checkout page all at once. Passing data through layers of props makes the code hard to maintain. The answer is global state with Pinia, Vue's official store.
Episode 7 covers Pinia inside Nuxt: how to enable its module, the two ways to define a store, plugins to extend stores, and the trickiest SSR issue — how to make sure state doesn't leak between users and stays correctly synchronized during hydration.
Install Pinia's official Nuxt module:
npm install @pinia/nuxt piniaexport default defineNuxtConfig({
modules: ["@pinia/nuxt"],
})Once the module is registered, you can create stores in the app/stores folder and use them anywhere without importing the module — stores that are called get created and shared automatically.
Let's create a shopping cart store for the belajar-shop project:
export const useKeranjangStore = defineStore("keranjang", {
state: () => ({
items: [] as { id: string; nama: string; harga: number }[],
}),
getters: {
total: (state) =>
state.items.reduce((jumlah, item) => jumlah + item.harga, 0),
},
actions: {
tambah(item: { id: string; nama: string; harga: number }) {
this.items.push(item)
},
kosongkan() {
this.items = []
},
},
})defineStore("keranjang", {...}) defines a store with state, getters, and actions. Getters compute derived values, actions hold state mutations — both can be used from any component.
Besides the options syntax above, Pinia supports a setup syntax that resembles components:
export const useKatalogStore = defineStore("katalog", () => {
const produk = ref([])
const total = computed(() => produk.value.length)
async function muat() {
produk.value = await $fetch("/api/produk")
}
return { produk, total, muat }
})Choose based on complexity: the options store is more declarative and easy to read, the setup store is more flexible for logic that uses computed values and helper functions.
const keranjang = useKeranjangStore()
function tambahKeKeranjang(produk) {
keranjang.tambah({ id: produk.id, nama: produk.nama, harga: produk.harga })
}useKeranjangStore() is called in a component to access the store instance. All components using the same store stay automatically in sync.
A Pinia plugin is a function run for every store created. It's used to add global behavior such as logging or persistence:
export default defineNuxtPlugin(() => {
const pinia = usePinia()
pinia.use((context) => {
context.store.$subscribe((mutation, state) => {
console.log("Store berubah", mutation.type)
})
})
})pinia.use((context) => {...}) registers a plugin that receives every new store. Here $subscribe is used to watch state changes — useful for analytics or logging.
For very small, local state, Nuxt provides useState — a composable for SSR-safe shared state:
export function usePromo() {
return useState("promo", () => "GRATIS-ONGKIR")
}useState("promo", () => "GRATIS-ONGKIR") creates reactive state shared between components and safe to use on both the server and the client. Use this for light data, and Pinia for complex business state with getters and actions.
During SSR, a single request can handle many users at the same time. If global state is initialized outside components, data between users can get mixed up. That's why Pinia stores in Nuxt are created per-request — every request gets its own store instance, so there is no data leakage.
Pinia in Nuxt delivers state from the server to the client through the payload. The key to success: store state must be initialized the same way on the server and the client.
if (import.meta.server) {
const keranjang = useKeranjangStore()
await keranjang.muatDariServer()
}The if (import.meta.server) pattern above loads initial data only on the server, then the state automatically synchronizes to the client during hydration. Avoid reading user data asynchronously directly in store state without consistent initialization.
Episode 7 completes Nuxt's state layer: Pinia as global state management with options and setup stores, plugins to extend stores, useState for lightweight shared state, and the important understanding that stores in Nuxt are created per-request so they don't leak between users during SSR.
Key takeaways:
@pinia/nuxt module and keep stores in app/stores.defineStore supports options and setup syntax; choose based on your needs.pinia.use and can add global behavior.useState is a lightweight SSR-safe alternative for shared state.In the next episode, episode 8, we will discuss configuration and runtime config — managing nuxt.config.ts, runtime config for public and private env variables, module configuration, and build optimizations and feature flags. Your store will start being configured properly.