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
| Where | What |
|---|---|
| Store | Data needed by several pages and the code that changes it |
| Component | Form state, whether a menu is open — what only it needs |
| Composable | Logic 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.