State management in Vue.js using Pinia

Getters are similar to computed properties in Vue.js. They allow you to derive or compute new state values based on the store’s existing state.

October 14, 20245 min read

One of the most vital aspects of frontend development is state management. State management is the process of consistently persisting the status of variables or user interface controls such as buttons and text fields across multiple user interfaces or components. Simply put, what components need to know what, at what time. In simple projects it is quite easy, but as the complexity of the application increases more components are involved and it becomes a pain in the ass, most cases as the complexity grows, developers resort to state management libraries like Pinia.

How to manage state in fairly simple projects

In a case where you want to manage state in a simple project, the logical way would be to use props. That would mean passing a value from the parent component to its children and since Vue’s prop system follows a one-way data flow model where data flows from parent to child, the child component cannot directly modify a prop. It has to notify the parent about the changes using custom events.

Parent Component

<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>Parent Component</h1>
    <p>Parent Count: {{ count }}</p>
    <ChildComponent :count="count" @increment="incrementCount" />
  </div>
</template>

<script>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent,
  },
  setup() {
    // State (reactive variable) in the parent component
    const count = ref(0);

    // Method to increment count
    const incrementCount = () => {
      count.value++;
    };

    return {
      count,
      incrementCount,
    };
  },
};
</script>

Breakdown

The count is passed down as a prop to ChildComponent while he incrementCount method is passed as a custom event listener (@increment), which will be triggered by the child component.

Child Component

<!-- ChildComponent.vue -->
<template>
  <div>
    <h2>Child Component</h2>
    <p>Child Count (from parent): {{ count }}</p>
    <button @click="emitIncrement">Increment Count</button>
  </div>
</template>

<script>
import { defineComponent } from 'vue';

export default defineComponent({
  props: {
    count: {
      type: Number,
      required: true,
    },
  },
  setup(props, { emit }) {
    // Emit custom event to parent to trigger the count increment
    const emitIncrement = () => {
      emit('increment');
    };

    return {
      emitIncrement,
    };
  },
});
</script>

Breakdown

The props (count) are received in the setup() function using props, the emitIncrement method triggers the custom event by calling emit('increment'), informing the parent to increment the count. In this case the props.count is displayed in the template but is not modified directly by the child component.

Quite simple, but for a case where you have multiple sibling components each with they own nested children it becomes complex.

Pinia

Pinia is a state management library built specifically for Vue.js, it provides a flexible and intuitive way to manage state within Vue applications, offering a more streamlined alternative to Vuex. It is built on top of vue’s composition API which makes working with states more reactive.

Try Kodaschool for free

Click below to sign up and get access to free web, android and iOs challenges.

Sign Up

Getting Started with Pinia

To get started with Pinia we need to first of all install it by running this command in your project.

npm install pinia

After installation we need to initialize and register Pinia in the main entry file( main.js or main.ts )

// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';

const app = createApp(App);
const pinia = createPinia(); // Create the Pinia instance

app.use(pinia); // Register Pinia with the Vue app
app.mount('#app');

Creating a Pinia store

In Pinia, state is organized into stores that define it and the actions that can be performed on that state.

State

State is where you store your application’s reactive data in Pinia. It acts as a single source of truth for your components, holding all shared state that multiple components may need to access. Pinia automatically makes state reactive, meaning any changes to the state are immediately reflected in the components using it.

Actions

Actions are functions defined in the store that allow you to modify the state or perform side effects such as making API request.

Getters

Getters are similar to computed properties in Vue.js. They allow you to derive or compute new state values based on the store’s existing state. Getters are cached and will only recompute when their dependencies change. Getters are reactive, meaning that if the state they depend on changes, the computed value automatically updates wherever it is used.

Converting our previous example using pinia

First, we need to create a Pinia store that manages the count state and the increment action.

// stores/counter.js
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useCounterStore = defineStore('counter', () => {
  // State: defining a reactive state variable
  const count = ref(0);

  // Action: defining a method to increment the count
  const increment = () => {
    count.value++;
  };

  // Returning state and actions
  return {
    count,
    increment,
  };
});

Next we need to convert the parent component to interact with the Pinia store instead of maintaining its own local state.

<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>Parent Component</h1>
    <p>Parent Count: {{ count }}</p>
    <ChildComponent />
  </div>
</template>

<script>
import { useCounterStore } from './stores/counter';
import ChildComponent from './ChildComponent.vue';
import { useStore } from 'pinia';

export default {
  components: {
    ChildComponent,
  },
  setup() {
    // Accessing the Pinia store
    const counterStore = useCounterStore();

    return {
      count: counterStore.count, // Accessing the state from the store
    };
  },
};
</script>

The child component can now directly interact with the store by accessing and modifying the shared state.

<!-- ChildComponent.vue -->
<template>
  <div>
    <h2>Child Component</h2>
    <p>Child Count (from store): {{ count }}</p>
    <button @click="increment">Increment Count</button>
  </div>
</template>

<script>
import { useCounterStore } from './stores/counter';

export default {
  setup() {
    // Accessing the Pinia store
    const counterStore = useCounterStore();

    return {
      count: counterStore.count,   // Accessing the state from the store
      increment: counterStore.increment, // Action to increment the count
    };
  },
};
</script>

Conclusion

Pinia simplifies state management by creating a centralized state, a single point of truth, This allows both the parent and child components to access and manipulate the same piece of state without needing to pass props or emit events. It also creates a shared store where both the parent and child components use the useCounterStore() function to access the store. They can directly read the count value and call the increment action. Pinia also ensures that any changes to the count value in the store are reactive. As a result, both components automatically update when the state changes.

Peace!

Jackson Obere

About Jackson Obere

I enjoy working with front-end technologies like Vue, React, and Vanilla JavaScript, with some Python on the side. I love building interactive web experiences and breaking down tricky concepts to make them easier to understand. I'm always curious and enjoy learning new things in the ever-evolving tech space.