In this tutorial, we’ll walk through the process of building a simple blog application using Vue 3, Vite, and Vuex.
A little bit about Vuex
Vuex is the state management library for Vue.js, and it’s particularly useful for managing the state of your application in a centralized manner. let’s take an example when we building a large application, just like in the blog post. It’s become very hard to maintain data among its components. We have to use one particular piece of data in many components. Passing props can be difficult for deeply nested components. And it is also very tedious to communicate from a child component to a parent component using events.
Basically The goal of using Vuex is to get rid of these problems. Vuex facilitates centralized data management in large-scale applications. While it introduces additional concepts and boilerplate code, it offers a structured and organized approach to handling shared state, enhancing long-term maintainability and scalability. The trade-off lies in the initial investment in understanding these concepts and writing boilerplate code, which pays off by streamlining state management as applications grow in complexity. So let’s jump into the code to better understand.
Prerequisites
Before we begin, make sure you have Node.js installed on your machine. If not, you can download and install it from nodejs.org.
Step 1: Setup a new Vite project
# Create a new Vite project
npm create vite my-vuex-blog --template vue
# Move into the project directory
cd my-vuex-blog
# Install dependencies
npm install
Step 2: Install Vuex
npm install vuex
Step 3: First Go with Simple example
Go to main.js file and configure the store.
import { createApp } from "vue";
import { createStore } from "vuex";
import "./style.css";
import App from "./App.vue";
const store = createStore({
state: {
counter: 0,
},
});
const app = createApp(App);
app.use(store);
app.mount("#app");
For understanding we define the store in main.js but later on I separate the store inside the store folder. We create a state with initial counter 0. When we click a button the counter will be incremented.
Now open App.js file and cleared up all its data. Then we write this code for incrementing the counter.
<script setup>
</script>
<script>
export default {
methods: {
increment() {
this.$store.state.counter++;
},
},
};
</script>
<template>
<div>
<p>{{ this.$store.state.counter }}</p>
<button @click="increment">Counter</button>
</div>
</template>
<style scoped>
</style>
If we go to browser and click the Counter button we can see the counter is incrementing. We use the instance variable this.$store.state of the store.
Blog post app
Now we start with the actually code how we get more benefit using Vuex in fashionable way.
Few points are considered:
- State: This is single object contains all your application level state/data and serves as the “single source of truth.” means servers globally throughout the application’s components.
- Getters: Getting some pieces of state or computed data from state.
- Mutation: Change the state or update the state.
- Actions: Called from app components commit a mutation.
Let’s create first store of our app. src/store/index.js
import { createStore } from "vuex";
export default createStore({
state: {
posts: [],
},
getters: {
allPosts(state) {
return state.posts;
},
},
mutations: {
setPosts(state, posts) {
state.posts = posts;
},
newPost(state, post) {
state.posts.unshift(post);
},
makeSeen(state, postId) {
const updatePost = state.posts.find((post) => post.id === postId);
updatePost.seen = true;
},
},
actions: {
fetchPosts({ commit }) {
const posts = [
{
id: 1,
title: "First Post",
content: "This is the content of the first post.",
seen: false,
},
{
id: 2,
title: "Second Post",
content: "This is the content of the second post.",
seen: false,
},
];
commit("setPosts", posts);
},
addPost({ commit }, newPost) {
commit("newPost", newPost);
},
changeSeen({ commit }, postId) {
commit("makeSeen", postId);
},
},
});
We define an empty post array. Then return all posts using getters. setPosts mutation is for fetching the posts when app is mounted, newPost mutation is for adding new post, and last makeSeen is updating seen false to true. And all actions are committed to the mutations.
Next create store instance in main.js and configure with createApp
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import store from "./store";
createApp(App).use(store).mount("#app");
Lastly Create show, add and update functionalities in App component. Although you can create separate component for each one.
<template>
<div>
<div>
<div>
<h2>Add a post</h2>
<form>
<div>
<label for="blog-title">Title</label>
<input
type="text"
id="blog-title"
aria-describedby="emailHelp"
v-model="title"
/>
</div>
<div>
<label for="blog-content">Content</label>
<textarea
name=""
id="content"
cols="30"
rows="10"
v-model="content"
></textarea>
</div>
<button type="submit" @click="addPost">
Post
</button>
</form>
</div>
<!-- blog post lists -->
<div>
<h1>My Vuex Blog</h1>
<div v-for="post in posts" :key="post.id">
<div :id="'accordionPost' + post.id">
<div>
<h2
:class="
!post.seen ? 'border-start border-5 border-success' : ''
"
:id="'headingOne' + post.id"
>
<button
type="button"
data-bs-toggle="collapse"
:data-bs-target="generateTargetSelector(post.id)"
aria-expanded="true"
:aria-controls="'collapseOne' + post.id"
@click="changeSeen(post.id)"
>
{{ post.title }}
</button>
</h2>
<div
:id="'collapseOne' + post.id"
aria-labelledby="headingOne"
data-bs-parent="#accordionExample"
>
<div>
{{ post.content }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
title: "",
content: "",
};
},
computed: {
posts() {
return this.$store.getters.allPosts;
},
},
mounted() {
this.$store.dispatch("fetchPosts");
},
methods: {
addPost(e) {
e.preventDefault();
if (!this.title.trim() || !this.content.trim()) {
alert("add title and content please");
return false;
}
const post = {
id: Date.now(),
title: this.title,
content: this.content,
seen: false,
};
this.$store.dispatch("addPost", post);
this.title = "";
this.content = "";
},
generateTargetSelector(id) {
return `#collapseOne${id}`;
},
changeSeen(postId) {
this.$store.dispatch("changeSeen", postId);
},
},
};
</script>
We use bootstrap quick design. So we need to add bootstrap cdn in index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
rel="stylesheet" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>
Conclusion
I hope you enjoyed this simple blog post app. Now recommendation official state management library for Vue has changed to Pinia. Pinia has almost the exact same or enhanced API as Vuex 5. So you can try out. https://pinia.vuejs.org/