Creating Single Page Applications (SPAs) with Vue.js — A Complete Beginner’s Guide

3.08K 0 0 0 0

📘 Chapter 5: State Management with Pinia or Vuex

🧭 What You’ll Learn

By the end of this chapter, you’ll be able to:

  • Understand what state management is in a Vue SPA
  • Identify when and why you need a store like Pinia or Vuex
  • Set up and use Pinia in a Vue 3 application
  • Learn the basics of Vuex (for existing Vue 2/legacy projects)
  • Share data between components
  • Use actions, mutations (Vuex), getters, and modules
  • Choose the right tool for your project

🧠 What Is State Management?

In a Vue.js application, "state" refers to the data that your components rely on: user input, fetched data, UI flags, etc.

When apps grow, managing state becomes difficult because:

  • Props and events become hard to track
  • Components are deeply nested
  • Many parts of the app need access to the same data

This is where a centralized store comes in—Pinia and Vuex are tools that help manage this shared data in a predictable, scalable way.


📦 Common Use Cases for a Global Store

Use Case

Example

Authentication

Current user info across pages

Cart data

Add/remove items from anywhere

Theme settings

Toggle light/dark globally

App-wide notifications

Trigger toasts/alerts

Persistent form data

Store data during multi-step wizards


🧩 Pinia vs. Vuex: What's the Difference?

Feature

Pinia

Vuex

Vue version

Vue 3 (official replacement)

Vue 2/3

API Style

Composition API

Options API

Setup required

Minimal

Verbose

Learning curve

Easy

Moderate

Dev tools

Yes (integrates with Vue DevTools)

Yes

Bundle size

Smaller

Larger

TL;DR: Use Pinia for Vue 3. Use Vuex only for legacy Vue 2 apps or if already integrated.


Getting Started with Pinia (Vue 3)


Step 1: Install Pinia

bash

 

npm install pinia


Step 2: Register Pinia in main.js

js

 

import { createApp } from 'vue'

import App from './App.vue'

import { createPinia } from 'pinia'

 

const app = createApp(App)

app.use(createPinia())

app.mount('#app')


Step 3: Create a Store

Create src/stores/counter.js:

js

 

import { defineStore } from 'pinia'

 

export const useCounterStore = defineStore('counter', {

  state: () => ({

    count: 0

  }),

  getters: {

    doubleCount: (state) => state.count * 2

  },

  actions: {

    increment() {

      this.count++

    },

    reset() {

      this.count = 0

    }

  }

})


Step 4: Use the Store in Components

Example: Counter.vue

vue

 

<script setup>

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

const counter = useCounterStore()

</script>

 

<template>

  <div>

    <p>Count: {{ counter.count }}</p>

    <p>Double: {{ counter.doubleCount }}</p>

    <button @click="counter.increment">+</button>

    <button @click="counter.reset">Reset</button>

  </div>

</template>


🧠 What Makes Pinia Developer-Friendly?

  • Supports TypeScript out of the box
  • No need to use mapState, mapActions (unlike Vuex)
  • Reactive like Vue’s Composition API
  • Better DX for smaller and larger apps

🔒 Vuex Overview (Vue 2/Legacy Projects)


Step 1: Install Vuex

bash

 

npm install vuex@next

Step 2: Create a Vuex Store

js

 

// store/index.js

import { createStore } from 'vuex'

 

const store = createStore({

  state: {

    count: 0

  },

  getters: {

    doubleCount: (state) => state.count * 2

  },

  mutations: {

    increment(state) {

      state.count++

    },

    reset(state) {

      state.count = 0

    }

  },

  actions: {

    incrementAsync({ commit }) {

      setTimeout(() => {

        commit('increment')

      }, 500)

    }

  }

})

 

export default store

Step 3: Register in main.js

js

 

import store from './store'

createApp(App).use(store).mount('#app')

Step 4: Use Store in Components

vue

 

<template>

  <div>

    <p>Count: {{ $store.state.count }}</p>

    <button @click="$store.commit('increment')">+</button>

    <button @click="$store.dispatch('incrementAsync')">Async +</button>

  </div>

</template>


🎨 Pinia vs Vuex API Comparison

Feature

Pinia Example

Vuex Example

State

store.count

$store.state.count

Getter

store.doubleCount

$store.getters.doubleCount

Mutation/Action

store.increment()

$store.commit('increment')

Registration

defineStore()

createStore()

DevTool Tabs

“Pinia” tab

“Vuex” tab


📁 Suggested Store File Organization (Pinia)

bash

 

src/

── stores/

│   ── counter.js

│   ── auth.js

│   └── theme.js

Each file represents a separate domain module, keeping things modular and testable.


🔁 Sharing State Across Components

You can use the same store instance in multiple components:

vue

 

<script setup>

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

const counter = useCounterStore()

</script>

 

<template>

  <p>The count is {{ counter.count }}</p>

</template>

If one component updates the state, all other components reactively reflect that change.


🧠 Best Practices

  • Use modular stores (useAuthStore, useCartStore, etc.)
  • Use getters for derived state (like filters, calculations)
  • Use actions for async or complex logic
  • Avoid directly mutating state outside of actions
  • Keep UI logic (like show/hide modals) in the store only when shared globally

🧪 Example: Auth Store with Async Action

js

 

export const useAuthStore = defineStore('auth', {

  state: () => ({

    user: null,

    loading: false

  }),

  actions: {

    async login(email, password) {

      this.loading = true

      const response = await fetch('/api/login', {

        method: 'POST',

        body: JSON.stringify({ email, password })

      })

      const data = await response.json()

      this.user = data.user

      this.loading = false

    },

    logout() {

      this.user = null

    }

  }

})


🧪 Testing Pinia Stores

Testing Pinia is straightforward:

js

 

import { setActivePinia, createPinia } from 'pinia'

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

 

beforeEach(() => {

  setActivePinia(createPinia())

})

 

test('increment counter', () => {

  const counter = useCounterStore()

  counter.increment()

  expect(counter.count).toBe(1)

})


Recap: When to Use a Store


Situation

Use Pinia/Vuex?

Passing data across pages

Deeply nested components

Local state (checkbox toggle)

Shared UI behavior

(optional)

API result caching

Back

FAQs


❓1. What is a Single Page Application (SPA)?

Answer:
A Single Page Application is a web app that loads a single HTML file and dynamically updates the content without refreshing the page. This allows for a smoother user experience, similar to desktop or mobile apps.

❓2. Why should I use Vue.js to build an SPA?

Answer:
Vue.js is lightweight, beginner-friendly, and has a powerful ecosystem. It supports component-based architecture and works seamlessly with Vue Router for SPA navigation. It’s a great choice for fast, reactive, and scalable SPAs.

❓3. What are the essential tools needed to build a Vue.js SPA?

Answer:

  • Vue CLI or Vite (project scaffolding)
  • Vue Router (for client-side navigation)
  • Pinia or Vuex (for state management)
  • Single File Components (.vue)
  • Axios or Fetch API (for server communication, if needed)

❓4. How does routing work in a Vue.js SPA?

Answer:
Vue Router handles navigation in the browser without reloading pages. It maps URL paths to Vue components and updates the view dynamically using the browser’s History API.

❓5. Can I use Vue.js without Vue Router for an SPA?

Answer:
Technically yes, but it’s not recommended. Vue Router provides essential SPA features like URL mapping, history handling, navigation guards, and lazy loading.

❓6. What is the difference between Vuex and Pinia?

Answer:
Both are state management libraries. Vuex is the older, more complex option, while Pinia is the official replacement for Vue 3—lighter, easier to use, and modular.

❓7. Can I build an SPA with Vue using only CDN links?

Answer:
Yes, but it's only suitable for small-scale SPAs. For larger projects, using the Vue CLI or Vite offers better file organization, hot reloading, and tooling support.

❓8. How do I handle SEO with a Vue SPA?

Answer:
SPAs typically have poor SEO out of the box because content is rendered via JavaScript. Use pre-rendering (e.g., with Prerender.io) or switch to Server-Side Rendering (SSR) with Nuxt.js for better SEO.

❓9. What hosting options are available for deploying Vue SPAs?

Answer:
You can deploy your Vue SPA on:

  • Netlify (easy integration with Git)
  • Vercel
  • Firebase Hosting
  • GitHub Pages
  • Render or DigitalOcean App Platform

❓10. Is Vue.js suitable for large-scale Single Page Applications?

Answer:
Yes. Vue.js is modular and scalable. With features like lazy loading, component splitting, Pinia/Vuex for state management, and Vue Router, it can power large, production-ready SPAs effectively.