State manage is a necessary thing in every frontend projects. Sometimes we need to define some objects throughout the application globally. If we do this by own it could be a big messy work. Recently I've worked with Pinia in a Nuxt project. I've decided to share the basic things with you. So, let's start.
Project setup
Generate a Nuxt 3 project using this command.
npx nuxi@latest init <project-name>
I choose nuxt-pinia-example
as the project name. Also during generation it asks what package manager we want to use. I choose yarn
as the package manager.
Now I've deleted the app.vue from the root directory. And create the directory pages
and create the index.vue
file and put some html code inside <template>
attribute.
Now add another pages like page1.vue and page2.vue
Add Pinia in the project
First install Pinia.
npx nuxi@latest module add pinia
The Pinia module should be added in the nuxt.config.ts
file. If not then add it manually.
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
devtools: { enabled: true },
modules: ['@pinia/nuxt']
})
Define a store
In every state management libraries, the states, business logics, actions are defined in stores. In Nuxt, using Pinia we've to create the folder stores
in the root directory for defining stores. Let's define a store. Today I want to create a feature like counter. Let's define number.ts
under the stores folder.
Next we need the defineStore()
from the Pinia module. We can define a store like Vue's Option API or like Composition API. Let's go with Composition flavor.
export const useNumberStore = defineStore('number', () => {
});
State
Inside defineStore
, the Vue's ref() is also the state in Pinia too. Le't define one. Also use the export
keyword and return the state so the state is visible to other parts of the project.
export const useNumberStore = defineStore('number', () => {
const count = ref(0);
return {
count
}
});
Action
Function defined inside defineStore
are the actions. let's define one and manipulate the previously created state count
.
export const useNumberStore = defineStore('number', () => {
const count = ref(0);
const increment = () => {
count.value++;
}
return {
count,
increment
}
});
Let's try this from our index.vue page. Just import the useNumberStore and save it in a variable inside <script setup>
and use them in <template>
part.
<script setup>
const numberStore = useNumberStore();
</script>
<template>
<div>
<h1>The count is {{ numberStore.count }}</h1>
<button @click="numberStore.increment">increment</button>
</div>
</template>
If we click the increment button, the count will be increased because we call the increment action here in click event. We can see the store in devtools too.
Getters
Pinia getters are Vue's computed. When we have a complex computation, rather define in the <template>
we can define computed. Let's define one.
export const useNumberStore = defineStore('number', () => {
const count = ref(0);
const increment = () => {
count.value++;
}
const sqrt = computed(() => count.value * count.value);
return {
count,
increment,
sqrt
}
});
Use the sqrt getter like this
<script setup>
const numberStore = useNumberStore();
</script>
<template>
<div>
<h1>The count is {{ numberStore.count }}</h1>
<h1>The sqrt of the number is {{ numberStore.sqrt }}</h1>
<button @click="numberStore.increment">increment</button>
</div>
</template>
See the result
Two-way binding
Now, I want to send the number from a input. So, we can achieve the 2 way communication. Using v-model
we can easily do this. The store code.
export const useNumberStore = defineStore('number', () => {
const count = ref(0);
const increment = () => {
count.value++;
}
const sqrt = computed(() => count.value * count.value);
return {
count,
increment,
sqrt
}
});
The app.vue code
<script setup>
const numberStore = useNumberStore();
</script>
<template>
<div>
<input type="text" v-model="numberStore.count">
<h1>The count is {{ numberStore.count }}</h1>
<h1>The sqrt of the number is {{ numberStore.sqrt }}</h1>
<button @click="numberStore.increment">increment</button>
</div>
</template>
Use everywhere
Now use the number store in other pages like page1.vue and page2.vue pages. For example in page2.vue
<script setup>
const numberStore = useNumberStore();
</script>
<template>
<h1>The count in page 1 is: <span>{{ numberStore.count }}</span></h1>
<h1>The sqrt in page 1 is: <span>{{ numberStore.sqrt }}</span></h1>
</template>
<style>
span {
color: red;
}
</style>
For routing, In app.vue I've used NuxtLink
<script setup>
const numberStore = useNumberStore();
</script>
<template>
<div>
<ul>
<li><NuxtLink to="/page1">Page 1</NuxtLink></li>
<li><NuxtLink to="/page2">Page 2</NuxtLink></li>
</ul>
<input type="text" v-model="numberStore.count">
<h1>The count is {{ numberStore.count }}</h1>
<h1>The sqrt of the number is {{ numberStore.sqrt }}</h1>
<button @click="numberStore.increment">increment</button>
</div>
</template>
Now change the value of count from index page and check it in page1 or page2.
See the url and output. We get the updated state and getter.
Share as a component
You can see We're duplicating code in page1 and page2. We can reuse code using component. Let's define a component. Add the directory components
under the root directory. I've added one component and named it NumberComponent.vue
Inside the NumberComponent.vue, add our reusable code
<script setup>
const numberStore = useNumberStore();
</script>
<template>
<div>
<h1>The count in page 1 is: <span>{{ numberStore.count }}</span></h1>
<h1>The sqrt in page 1 is: <span>{{ numberStore.sqrt }}</span></h1>
</div>
</template>
<style>
span {
color: red;
}
</style>
Now use this component in page1 and page2 like this
<template>
<NumberComponent />
</template>
Conclusion
I hope you get a basic idea of the state management using Pinia in a Nuxt project. Hope you liked this. Happy coding.