Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Pinia: actions and using the store

An action is a store function that changes the store's data. The change logic lives in one place, and components only call the action: "load posts", "add a post". An action can be asynchronous and talk to an API.

Actions in the store

Extend src/stores/posts.js with loading and adding posts:

// src/stores/posts.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
 
const API = 'https://jsonplaceholder.typicode.com/posts'
 
export const usePostsStore = defineStore('posts', () => {
  const posts = ref([])
  const loading = ref(false)
  const error = ref(null)
 
  const count = computed(() => posts.value.length)
 
  async function fetchPosts() {
    loading.value = true
    error.value = null
    try {
      const response = await fetch(`${API}?_limit=10`)
      if (!response.ok) throw new Error(`Server responded ${response.status}`)
      posts.value = await response.json()
    } catch (e) {
      error.value = e.message
    } finally {
      loading.value = false
    }
  }
 
  function addPost(title, body) {
    const id = Math.max(0, ...posts.value.map((p) => p.id)) + 1
    posts.value.unshift({ id, title, body })
  }
 
  return { posts, loading, error, count, fetchPosts, addPost }
})

Actions are plain functions inside defineStore. They must be returned together with the data, otherwise components cannot see them.

Calling an action and storeToRefs

Rewrite src/components/PostList.vue to use the store:

<!-- src/components/PostList.vue -->
<script setup>
import { onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { usePostsStore } from '../stores/posts.js'
 
const postsStore = usePostsStore()
const { posts, loading, error } = storeToRefs(postsStore)
 
onMounted(() => {
  if (posts.value.length === 0) postsStore.fetchPosts()
})
</script>
 
<template>
  <p v-if="loading">Loading...</p>
  <p v-else-if="error">Error: {{ error }}</p>
  <ul v-else>
    <li v-for="post in posts" :key="post.id">
      <RouterLink :to="{ name: 'post', params: { id: post.id } }">{{ post.title }}</RouterLink>
    </li>
  </ul>
</template>

The home page shows ten posts from the API. storeToRefs turns the store's data into separate ref objects that can be destructured without losing reactivity. Actions are taken straight from the store — postsStore.fetchPosts(). The length check prevents reloading the list every time you return to the home page.

The new post form

Replace src/views/NewPostView.vue:

<!-- src/views/NewPostView.vue -->
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { usePostsStore } from '../stores/posts.js'
 
const postsStore = usePostsStore()
const router = useRouter()
const title = ref('')
const body = ref('')
 
function save() {
  if (title.value.trim() === '') return
  postsStore.addPost(title.value.trim(), body.value)
  router.push({ name: 'home' })
}
</script>
 
<template>
  <h2>New post</h2>
  <form @submit.prevent="save">
    <input v-model="title" placeholder="Title">
    <textarea v-model="body" placeholder="Text"></textarea>
    <button>Publish</button>
  </form>
</template>

Open /admin/new, enter a title and press "Publish" — the home page opens, the new post is first in the list, and the counter in the header goes up. The post lives only in the browser's memory: the test API does not store data, so it disappears after a reload.

What goes where

WhereWhat
StoreData needed by several pages and the code that changes it
ComponentForm state, whether a menu is open — what only it needs
ComposableLogic without shared state: a timer, useFetch

Common mistake

Taking an action through storeToRefs: const { fetchPosts } = storeToRefs(postsStore). storeToRefs picks only data and computed values and skips functions — fetchPosts turns out undefined. Call actions through the store or destructure the store directly: const { fetchPosts } = postsStore.

Thanks for reading!

Donate to the site — we work hard for you.