State Management with Pinia in Nuxt 3: How I've started

State Management with Pinia in Nuxt 3: How I've started

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.