Nuxt State Management — useState and Pinia in Nuxt 3
In this tutorial, you will learn about Nuxt State Management. We cover key concepts, practical examples, and best practices to help you master this topic.
Learn Nuxt 3 state management: useState for simple shared state and Pinia for complex state management with stores.
In this lesson, you'll understand how to share state across components with useState, and how to use Pinia for more structured state management.
What You'll Learn
How to use useState for SSR-safe shared state, install and configure Pinia, create Pinia stores, and use them in components.
Why It Matters
State management keeps your application data consistent across components. Nuxt provides both simple (useState) and advanced (Pinia) solutions.
flowchart TD
A[State Management] --> B[Simple State]
A --> C[Complex State]
B --> D[useState]
C --> E[Pinia]
D --> F[useState('key', () => initial)]
E --> G[defineStore]
E --> H[Actions, Getters]
style A fill:#00dc82,color:#fff
useState
Share reactive state across components with SSR support:
// composables/useCart.ts
export const useCart = () => {
const cart = useState('cart', () => []);
const addItem = (item) => {
cart.value = [...cart.value, item];
};
const removeItem = (id) => {
cart.value = cart.value.filter(i => i.id !== id);
};
const total = computed(() =>
cart.value.reduce((sum, item) => sum + item.price, 0)
);
return { cart, addItem, removeItem, total };
};
Usage in any component:
<script setup>
const { cart, addItem, total } = useCart();
</script>
<template>
<div>
<p>Cart: {{ cart.length }} items ({{ total }})</p>
<button @click="addItem({ id: 1, name: 'Widget', price: 10 })">Add</button>
</div>
</template>
Output: The cart state is shared across all components. SSR serializes the state so it persists after hydration.
Pinia Setup
npm install @pinia/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt']
});
Creating a Pinia Store
// stores/counter.ts
export const useCounterStore = defineStore('counter', () => {
// State
const count = ref(0);
// Getters
const doubleCount = computed(() => count.value * 2);
// Actions
function increment() { count.value++; }
function decrement() { count.value--; }
function reset() { count.value = 0; }
return { count, doubleCount, increment, decrement, reset };
});
Usage:
<script setup>
const counter = useCounterStore();
</script>
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">+</button>
<button @click="counter.decrement">-</button>
</div>
</template>
Pinia with Async Actions
Store with server data fetching:
// stores/posts.ts
export const usePostsStore = defineStore('posts', () => {
const posts = ref([]);
const currentPost = ref(null);
const loading = ref(false);
const error = ref(null);
async function fetchPosts() {
loading.value = true;
error.value = null;
try {
const data = await $fetch('/api/posts');
posts.value = data;
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
async function fetchPost(id: string) {
loading.value = true;
try {
const data = await $fetch(`/api/posts/${id}`);
currentPost.value = data;
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
return { posts, currentPost, loading, error, fetchPosts, fetchPost };
});
useState vs Pinia
| Feature | useState | Pinia |
|---|---|---|
| Complexity | Simple key-value | Full store |
| DevTools | Limited | Vue DevTools |
| SSR | Built-in | Built-in |
| TypeScript | Manual typing | Auto-generated |
| Best for | Small shared state | Complex app state |
Common Mistakes
- Using
refinstead ofuseStatefor shared state:refis local to the component. UseuseStatefor state shared across components. - Not using a unique key in
useState: The string key must be unique. Collisions cause unexpected shared state. - Mutating Pinia state directly: Use actions to modify state. Direct mutation works but bypasses DevTools tracking.
- Forgetting
@pinia/nuxtmodule: Without it, Pinia stores don't work in Nuxt. Install and add tomodules. - Not using Pinia's
$resetfor forms: Pinia stores don't have a built-in reset. Implement one manually if needed.
Practice Questions
What is the difference between
useStateandref? Answer:useStateis shared across components and SSR-safe.refis local to a single component instance.How do you define a Pinia store with the Composition API? Answer: Use
defineStore('name', () => { ... })with a setup function that returns state, getters, and actions.How do you use a Pinia store in a component? Answer: Call
const store = useStoreName()inside<script setup>. Access state asstore.propertyand actions asstore.action().When should you use Pinia instead of useState? Answer: For complex state with multiple actions, getters, async operations, or when you need Vue DevTools integration.
Challenge
Build a shopping cart with Pinia: add/remove items, quantity updates, coupon application, and tax calculation. Use getters for subtotal, tax, and total. Persist to localStorage.
Mini Project
Create a full-featured e-commerce storefront with: Pinia stores for cart, user auth, product catalog, and wishlist. Use useState for theme toggling. Connect actions to server routes.
FAQ
What's Next
Learn about Nuxt SEO and Meta Tags to optimize your Nuxt application for search engines.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro