Skip to content

State Management

Effective state management is essential for building maintainable and scalable frontend applications. By following best practices and leveraging tools like Pinia in Nuxt 3 or Zustand in Next, you can streamline state management and create robust and responsive user interfaces.

This section discusses the importance of state management, best practices, and references for implementing state management in Next and Nuxt.

Importance of State Management

State management allows you to maintain and synchronize the state of your application across different components. It provides a centralized location for storing and accessing shared data, leading to improved consistency, maintainability, and scalability of your codebase.

Benefits of State Management

  • Centralized State: Avoids prop drilling and keeps state management centralized.
  • Predictable State Changes: Provides a clear and predictable way to update application state.
  • Improved Scalability: Facilitates scaling of complex applications by managing state in a structured manner.
  • Enhanced Maintainability: Simplifies debugging and maintenance by isolating state-related logic.

Pinia in Nuxt 3

Nuxt 3 introduces Pinia as its recommended state management solution. Pinia offers a Vue 3 store inspired by Vuex but with improved reactivity and TypeScript support. It integrates seamlessly with Nuxt 3 and provides a robust solution for managing application state. The official documentation for using Pinia with Nuxt can be found here.

Installation

  • Install the package:
bash
yarn add @pinia/nuxt

Add the module to your Nuxt configuration:

typescript
// nuxt.config.ts

export default defineNuxtConfig({
  // ...
  modules: [
    // ...
    '@pinia/nuxt',
  ],
})

Creating a store

Stores are created in a stores/ directory, and defined by using Pinia's defineStore method.

In this example, we have created a store (useCounterStore) and given the store a name (counter). We have then defined our state property (count) with an initial value.

typescript
// stores/counter.ts

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
})

Using the store Pinia offers a few ways to access the store and maintain reactivity.

1. Store instance
In your component's setup(), import the store's useStore() method.

js
// components/MyCounter.vue

import { useCounterStore } from '@/stores/counter'

export default defineComponent({
  setup() {
    return {
      store: useCounterStore(),
    }
  },
})

You can now access state through the store instance:

js
// components/MyCounter.vue

<template>
 <p>Counter: {{ store.count }}</p>
</template>

2. Computed properties
To write cleaner code, you may wish to grab specific properties. However, destructuring the store will break reactivity.

Instead, we can use a computed property to achieve reactivity:

javascript
 // components/MyCounter.vue
export default defineComponent({
  setup() {
    const store = useCounterStore()

    // ❌ Bad (unreactive):
    const { count } = store

    // ✔️ Good:
    const count = computed(() => store.count)

    return { count }
  },
})

<template>
  <p>Counter: {{ store.count }}</p>
</template>

3. Extract via storeToRefs()
You can destructure properties from the store while keeping reactivity through the use of storeToRefs().

This will create a ref for each reactive property.

javascript
 // components/MyCounter.vue
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

export default defineComponent({
  setup() {
    const store = useCounterStore()

    // ❌ Bad (unreactive):
    const { count } = store

    // ✔️ Good:
    const { count } = storeToRefs(store)

    return { count }
  },
})

<template>
  <p>Counter: {{ store.count }}</p>
</template>

Actions

Adding an action
Actions are the equivalent of methods in components, defined in the store's actions property.

javascript
 // stores/counter.ts
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  actions: {
    increment() {
      this.count++
    },
  },
})

Using an action
In your component, extract the action from the store.

javascript
// components/MyCounter.vue
export default defineComponent({
  setup() {
    const store = useCounterStore()
    const { increment } = store
    const count = computed(() => store.count)
    return { increment, count }
  },
})

The action can easily be invoked, such as upon a button being clicked:

javascript
// components/MyCounter.vue
<template>
  <button type="button" @click="increment"></button>
</template>

Getters

Getters are the equivalent of computed in components, defined in the store's getters property.

Adding a getter
Pinia encourages the usage of the arrow function, using the state as the first parameter:

javascript
// stores/counter.ts
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    getCount: (state) => state.count,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

Using a getter
Similarly to state properties, getters need to be accessed in a way that maintains reactivity.

For instance, you could access it through the store instance:

javascript
 // components/MyCounter.vue
export default defineComponent({
  setup() {
    const store = useCounterStore()
    return { store }
  },
})

<template>
  <p>Counter: {{ store.getCount }}</p>
</template>

Or, by using a computed property:

javascript
 // components/MyCounter.vue
export default defineComponent({
  setup() {
    const store = useCounterStore()

    // ❌ Bad (unreactive):
    const { getCount } = store

    // ✔️ Good:
    const getCount = computed(() => store.getCount)

    return { getCount }
  },
})
javascript
 // components/MyCounter.vue
<template>
  <p>Counter: {{ getCount }}</p>
</template>

Or, by using storeToRefs():

javascript
 // components/MyCounter.vue
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

export default defineComponent({
  setup() {
    const store = useCounterStore()

    // ❌ Bad (unreactive):
    const { getCount } = store

    // ✔️ Good:
    const { getCount } = storeToRefs(store)

    return { getCount }
  },
})
javascript
// components/MyCounter.vue
<template>
  <p>Counter: {{ getCount }}</p>
</template>

Zustand in Next.js

Zustand is a state management library for React applications. It is a simple and flexible state management solution that leverages React Hooks for managing state within components. Zustand is designed to be minimalistic, and it provides a small, focused API for managing application state without the need for a complex setup.
The official documentation for using Zustand can be found here.

Installation

  • Install the package:
bash
npm i zustand

Creating a store

Here, we have created a store (useCountStore) and defined our state properties (count and increase) with initial values.

javascript
// store/useCountStore.ts
import { create } from 'zustand'

const useCountStore = create((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
}))

Binding your component

Below, we have created a component where our selected state will be used. By importing the created store and accessing the state properties, we can manage the state from this component. On clicking the button, the component will re-render and the state will be updated.

javascript
// components/Counter.tsx
"use client"

import React from 'react'
import { useCountStore } from '../../store/useCountStore'

const Counter = () => {
  const count = useCountStore((state) => state.count)
  const increase = useCountStore((state) => state.increase)
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increase}>Increase Count</button>
    </div>
  )
}

export default Counter

Was this page helpful?

Happy React is loading...