Vue.js Vuex Actions
Introduction
When managing state in Vue.js applications, Vuex provides a structured way to handle data flow. While mutations are responsible for synchronous state changes, Vuex actions handle asynchronous operations before committing mutations. Actions are a crucial part of the Vuex architecture that allow you to perform complex logic, API calls, and other asynchronous tasks while maintaining a predictable state management pattern.
In this guide, we'll explore how actions work within Vuex, why they're important, and how to implement them effectively in your Vue.js applications.
Understanding Vuex Actions
What Are Actions?
Actions are methods defined in the Vuex store that:
- Can perform asynchronous operations
- Commit mutations when those operations complete
- Can access the store context (state, getters, and other actions)
- Can be triggered from components using
this.$store.dispatch()
Unlike mutations, actions don't directly change the state. Instead, they coordinate asynchronous operations and commit mutations when those operations complete.
Why Use Actions?
Actions are essential because:
- Separation of concerns: They separate asynchronous logic from state mutations
- Error handling: Provide a centralized place to manage API errors
- Reusability: Can be reused across multiple components
- Testability: Make it easier to test asynchronous processes
Defining Actions in Vuex
Let's create a basic Vuex store with actions:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
todos: [],
loading: false,
error: null
},
mutations: {
SET_TODOS(state, todos) {
state.todos = todos
},
SET_LOADING(state, status) {
state.loading = status
},
SET_ERROR(state, error) {
state.error = error
}
},
actions: {
// Basic action with context object
fetchTodos({ commit }) {
// Set loading state
commit('SET_LOADING', true)
// Perform API call
fetch('https://jsonplaceholder.typicode.com/todos')
.then(response => response.json())
.then(todos => {
// Commit mutation to update state
commit('SET_TODOS', todos)
commit('SET_LOADING', false)
})
.catch(error => {
commit('SET_ERROR', error)
commit('SET_LOADING', false)
})
}
}
})
In this example:
- We defined a
fetchTodos
action that retrieves a list of todos from an API - The action commits mutations to update the loading state and store the fetched data
- Error handling is included to update the error state if something goes wrong
The Action Context Object
When defining actions, the first parameter is the context object, which provides access to the same set of methods/properties on the store instance:
actions: {
exampleAction(context) {
// context object provides access to:
context.state // -> same as store.state
context.getters // -> same as store.getters
context.commit // -> same as store.commit
context.dispatch // -> same as store.dispatch
}
}
Most commonly, you'll use ES6 destructuring to simplify the code:
actions: {
exampleAction({ state, getters, commit, dispatch }) {
// Now you can use these methods directly
commit('SOME_MUTATION')
}
}
Using Actions in Vue Components
There are several ways to dispatch actions from your components:
1. Using $store.dispatch
<template>
<div>
<button @click="loadTodos">Load Todos</button>
<div v-if="loading">Loading...</div>
<ul v-else>
<li v-for="todo in todos" :key="todo.id">{{ todo.title }}</li>
</ul>
</div>
</template>
<script>
export default {
computed: {
todos() {
return this.$store.state.todos
},
loading() {
return this.$store.state.loading
}
},
methods: {
loadTodos() {
this.$store.dispatch('fetchTodos')
}
}
}
</script>
2. Using mapActions
Helper
The mapActions
helper makes it easier to access multiple actions:
<template>
<div>
<button @click="fetchTodos">Load Todos</button>
<button @click="addTodo(newTodo)">Add Todo</button>
<!-- ... -->
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex'
export default {
data() {
return {
newTodo: { title: 'New task', completed: false }
}
},
computed: {
...mapState(['todos', 'loading'])
},
methods: {
...mapActions(['fetchTodos', 'addTodo'])
}
}
</script>
Actions with Payloads
Actions can accept additional arguments (payloads) when dispatched:
// In the store
actions: {
addTodo({ commit }, todoItem) {
commit('SET_LOADING', true)
return fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST',
body: JSON.stringify(todoItem),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
})
.then(response => response.json())
.then(newTodo => {
commit('ADD_TODO', newTodo)
commit('SET_LOADING', false)
return newTodo // Returning for promise chaining
})
.catch(error => {
commit('SET_ERROR', error)
commit('SET_LOADING', false)
throw error // Rethrow for promise chaining
})
}
}
// In a component
methods: {
createTodo() {
this.$store.dispatch('addTodo', {
title: this.newTodoText,
completed: false
}).then(() => {
this.newTodoText = '' // Clear input on success
}).catch(err => {
this.errorMessage = 'Failed to add todo'
})
}
}
Returning Promises from Actions
Actions can return Promises, which is useful for knowing when an action has completed:
actions: {
fetchTodos({ commit }) {
commit('SET_LOADING', true)
// Return the promise
return fetch('https://jsonplaceholder.typicode.com/todos')
.then(response => response.json())
.then(todos => {
commit('SET_TODOS', todos)
commit('SET_LOADING', false)
return todos // Return for promise chaining
})
.catch(error => {
commit('SET_ERROR', error)
commit('SET_LOADING', false)
throw error // Rethrow for promise chaining
})
}
}
Now in your component, you can chain .then()
and .catch()
:
methods: {
loadTodos() {
this.$store.dispatch('fetchTodos')
.then(todos => {
console.log('Todos loaded successfully:', todos.length)
this.showNotification('Todos loaded!')
})
.catch(error => {
console.error('Failed to load todos:', error)
})
}
}
Composing Actions
Actions can call other actions using the dispatch
method, allowing you to compose complex behavior:
actions: {
async loadDashboard({ dispatch }) {
// Dispatch multiple actions in parallel
const [userData, statsData, notificationsData] = await Promise.all([
dispatch('fetchUserProfile'),
dispatch('fetchStats'),
dispatch('fetchNotifications')
])
// Process combined results
return {
user: userData,
stats: statsData,
notifications: notificationsData
}
},
async fetchUserProfile({ commit }) {
// Action implementation...
},
async fetchStats({ commit }) {
// Action implementation...
},
async fetchNotifications({ commit }) {
// Action implementation...
}
}
Using Async/Await with Actions
Modern JavaScript provides the async/await
syntax, making asynchronous code easier to read and write:
actions: {
// Using async/await for cleaner asynchronous code
async fetchTodos({ commit }) {
commit('SET_LOADING', true)
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos')
const todos = await response.json()
commit('SET_TODOS', todos)
return todos
} catch (error) {
commit('SET_ERROR', error.message)
throw error
} finally {
commit('SET_LOADING', false)
}
}
}
Real-World Example: Authentication Flow
Let's implement a more complex real-world example of Vuex actions for user authentication:
// store/modules/auth.js
import authApi from '@/api/auth'
const state = {
token: localStorage.getItem('token') || null,
user: JSON.parse(localStorage.getItem('user') || 'null'),
loading: false,
error: null
}
const mutations = {
AUTH_REQUEST(state) {
state.loading = true
state.error = null
},
AUTH_SUCCESS(state, { token, user }) {
state.token = token
state.user = user
state.loading = false
},
AUTH_ERROR(state, error) {
state.error = error
state.loading = false
},
LOGOUT(state) {
state.token = null
state.user = null
}
}
const actions = {
// Login action
async login({ commit, dispatch }, credentials) {
try {
commit('AUTH_REQUEST')
const { token, user } = await authApi.login(credentials)
// Store the token in localStorage
localStorage.setItem('token', token)
localStorage.setItem('user', JSON.stringify(user))
// Set the auth header for future requests
authApi.setAuthHeader(token)
commit('AUTH_SUCCESS', { token, user })
// Fetch user data after successful login
dispatch('fetchUserData')
return { token, user }
} catch (error) {
commit('AUTH_ERROR', error.message)
localStorage.removeItem('token')
localStorage.removeItem('user')
throw error
}
},
// Logout action
logout({ commit }) {
return new Promise(resolve => {
commit('LOGOUT')
localStorage.removeItem('token')
localStorage.removeItem('user')
authApi.clearAuthHeader()
resolve()
})
},
// Check if token is still valid
async checkAuth({ commit, state }) {
if (!state.token) return Promise.reject('No token found')
try {
commit('AUTH_REQUEST')
const user = await authApi.verifyToken(state.token)
commit('AUTH_SUCCESS', { token: state.token, user })
return user
} catch (error) {
commit('AUTH_ERROR', error.message)
commit('LOGOUT')
localStorage.removeItem('token')
localStorage.removeItem('user')
throw error
}
},
// Fetch user data action
async fetchUserData({ commit, state }) {
try {
const userData = await authApi.getUserData(state.user.id)
commit('UPDATE_USER_DATA', userData)
return userData
} catch (error) {
console.error('Failed to fetch user data:', error)
throw error
}
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
Using these actions in a Login component:
<template>
<form @submit.prevent="handleLogin">
<div class="form-group">
<label>Email</label>
<input type="email" v-model="email" required />
</div>
<div class="form-group">
<label>Password</label>
<input type="password" v-model="password" required />
</div>
<div v-if="error" class="error">{{ error }}</div>
<button type="submit" :disabled="loading">
{{ loading ? 'Logging in...' : 'Log In' }}
</button>
</form>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
data() {
return {
email: '',
password: ''
}
},
computed: {
...mapState('auth', ['loading', 'error'])
},
methods: {
...mapActions('auth', ['login']),
async handleLogin() {
try {
await this.login({
email: this.email,
password: this.password
})
this.$router.push('/dashboard')
} catch (error) {
// Error is already handled in the store
console.error('Login failed')
}
}
}
}
</script>
Best Practices for Vuex Actions
-
Keep actions focused: Each action should serve a specific purpose.
-
Handle errors consistently: Implement a standard pattern for error handling.
-
Use async/await for readability: Modern JavaScript makes asynchronous code more readable.
-
Return promises: Allow components to react to action completion or failure.
-
Consider using modules: For larger applications, organize actions into Vuex modules.
-
Use action composition: Break down complex workflows into smaller, reusable actions.
-
Separate API logic: Consider creating a separate API service layer that your actions can call.
// api/todos.js - API service layer
export default {
async fetchTodos() {
const response = await fetch('https://jsonplaceholder.typicode.com/todos')
if (!response.ok) throw new Error('Failed to fetch todos')
return response.json()
},
async addTodo(todo) {
const response = await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST',
body: JSON.stringify(todo),
headers: {
'Content-type': 'application/json'
}
})
if (!response.ok) throw new Error('Failed to add todo')
return response.json()
}
}
// store/todos.js - Vuex module using the API service
import todosApi from '@/api/todos'
const actions = {
async fetchTodos({ commit }) {
commit('SET_LOADING', true)
try {
const todos = await todosApi.fetchTodos()
commit('SET_TODOS', todos)
return todos
} catch (error) {
commit('SET_ERROR', error.message)
throw error
} finally {
commit('SET_LOADING', false)
}
}
}
Summary
Vuex actions are a powerful mechanism for handling asynchronous operations in your Vue.js applications. They provide a structured way to perform complex logic, API calls, and other side effects while maintaining the predictability of your state management.
Key points to remember:
- Actions handle asynchronous operations; mutations handle synchronous state changes
- Actions are dispatched from components using
this.$store.dispatch()
- Actions can return promises for handling success and error cases
- Modern JavaScript features like async/await make actions more readable
- Actions can be composed by dispatching other actions
- For larger applications, organize actions into Vuex modules
By following the patterns and practices outlined in this guide, you'll be able to implement robust and maintainable state management in your Vue.js applications.
Additional Resources and Exercises
Resources
Exercises
-
Todo App with API: Create a Todo application that uses Vuex actions to fetch, create, update, and delete todos from a remote API.
-
Authentication System: Implement a complete authentication system with login, logout, and registration using Vuex actions.
-
Shopping Cart: Build a shopping cart that loads products from an API and manages the cart state through Vuex actions.
-
Action Refactoring Challenge: Take an existing application with API calls directly in components and refactor it to use Vuex actions.
-
Error Handling System: Create a centralized error handling system using Vuex actions that can display appropriate error messages to users.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)