Fundamentals of Vuex and State Management

Fundamentals of Vuex and State Management

Картинка к публикации: Fundamentals of Vuex and State Management

Introduction

Understanding State Management

As you embark on the journey of developing modern web applications, it's impossible not to encounter the challenge of state management. Imagine a vast and intricate machine where every component plays its role, interacting precisely with other parts. In this context, state management can be compared to the conductor of an orchestra, maintaining harmony and coherence among all the instruments.

So, what is state? State is a set of data that defines the current status of an application at any given moment. This could be user data, interface settings, or the result of an asynchronous server request. In small applications, managing state can be relatively simple: components exchange data directly through props and events. But as the application grows, this scheme starts to resemble a tangled ball of yarn that's increasingly difficult to unravel.

This is where the concept of state management comes into play. By applying it correctly, we can simplify working with data and make the code more predictable and easier to maintain. In Vue 3, the library used for this purpose is Vuex.

Why is state management so important?

  • Centralized Data Store: Instead of scattering data across various components, we create a single store where all the data resides in one place. This makes it easy to track state changes and simplifies debugging.
  • Data Transparency: All state changes occur through explicitly defined methods (actions and mutations). This makes the process of changes transparent and predictable, eliminating the chaos of uncontrolled modifications.
  • Simplifying Data Sharing Between Components: With Vuex, components can access data from the centralized store without the need to pass it through long chains of props. This significantly simplifies the application's architecture.
  • Support for Scalability: As the application grows, state management remains structured thanks to the separation of logic into modules in Vuex. Each module is responsible for its own part of the state, making the code more organized.

The next step is to delve into the Vuex library itself and explore its fundamental principles in practice...

Introduction to Vuex: 

Entering the world of Vuex is like diving into a meticulously organized realm where every action is strictly regulated and every element has its designated role. Vuex, being the official state management library for Vue.js, offers us a tool to organize data and its processing. Let's break down its key components: state, getters, mutations, and actions.

State: The Heart of the Store

State is the central place where all your application's data is stored. Imagine a treasure chest holding everything valuable—from user information to interface settings. State provides a single source of truth for all components in the application.

// store/index.js

const state = {
  user: null,
  settings: {},
  items: []
};

Getters: The Windows to Your Data

Getters can be likened to the windows in our castle, through which we can observe the contents of the treasure chest (state). Getters allow you to extract and process data from the state without directly modifying the state itself.

// store/index.js

const getters = {
  isLoggedIn(state) {
    return !!state.user;
  },
  getUserSettings(state) {
    return state.settings;
  }
};

Mutations: The Guardians of Change

Mutations are the only entities that have the authority to modify the state. Like guardians at the gates of a castle, they ensure that all changes occur according to the rules and are predictable. Mutations are always synchronous and accept the current state as the first parameter.

// store/index.js

const mutations = {
  setUser(state, user) {
    state.user = user;
  },
  updateSettings(state, settings) {
    state.settings = { ...state.settings, ...settings };
  }
};

Actions: The Messengers of Asynchronous Tasks

Actions serve as the messengers of the king (your application), executing asynchronous tasks before delivering the results to mutations for final state changes. Think of them as envoys who handle complex operations like fetching data from a server or processing user input over time. They ensure that once these tasks are completed, the state is updated appropriately through mutations. Actions can dispatch other actions or commit mutations and accept the store context as a parameter.

// store/index.js

const actions = {
  async fetchUser({ commit }) {
    const user = await api.getUser();
    commit('setUser', user);
  },
  async saveSettings({ commit }, settings) {
    const updatedSettings = await api.saveSettings(settings);
    commit('updateSettings', updatedSettings);
  }
};

Each element plays its role in maintaining order and consistency within our Vuex store.

Examples of Simple Vuex Use Cases

Transitioning from theory to practice, let's look at some examples of using Vuex in real-world applications. These step-by-step guides will help you solidify your knowledge and feel confident when building your first Vue 3 application using Vuex.

Let's start by creating a simple to-do list application. To do this, we'll first set up our Vuex store.

  1. Install Vuex:
npm install vuex@next --save

  2. Create the store/index.js file and define the basic store:

// store/index.js

import { createStore } from 'vuex';

const state = {
  tasks: []
};

const getters = {
  allTasks: (state) => state.tasks,
};

const mutations = {
  addTask(state, task) {
    state.tasks.push(task);
  },
  removeTask(state, taskIndex) {
    state.tasks.splice(taskIndex, 1);
  }
};

const actions = {
  async addNewTask({ commit }, task) {
    // Здесь можно добавить асинхронный код для взаимодействия с API
    commit('addTask', task);
  },
  async deleteTask({ commit }, taskIndex) {
    // Здесь можно добавить асинхронный код для взаимодействия с API
    commit('removeTask', taskIndex);
  }
};

export default createStore({
  state,
  getters,
  mutations,
  actions
});

Now let's integrate the store we just created into our Vue application.

 3. Open main.js and connect the store:

// main.js

import { createApp } from 'vue';
import App from './App.vue';
import store from './store';

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

Next, we'll create a component to display the list of tasks and a form for adding new tasks.

  4. Create the TodoList.vue component:

<template>
  <div>
    <h1>Список задач</h1>
    <ul>
      <li v-for="(task, index) in tasks" :key="index">
        {{ task }}
        <button @click="removeTask(index)">Удалить</button>
      </li>
    </ul>
    <input v-model="newTask" placeholder="Добавить новую задачу" />
    <button @click="addTask">Добавить</button>
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex';

export default {
  data() {
    return {
      newTask: ''
    };
  },

  methods: {
    ...mapActions(['addNewTask', 'deleteTask']),

    addTask() {
      if (this.newTask.trim()) {
        this.addNewTask(this.newTask);
        this.newTask = '';
      }
    },

    removeTask(index) {
        this.deleteTask(index);
    }
  },

  computed: {
    ...mapGetters(['allTasks']),

    tasks() {
      return this.allTasks;
    }
  }
};
</script>

<style scoped>
/* Ваши стили */
</style>

  5. Include the TodoList.vue component in the main application component (App.vue):

<template>
  <div id="app">
    <TodoList/>
  </div>
</template>

<script>
import TodoList from './components/TodoList.vue';

export default{
  components:{
    TodoList}
  };
</script>

<style scoped> /* ваши стили */ </style>

Now you have a fully functional Vue 3 application with state management using Vuex! This example demonstrated the basic steps for setting up and using Vuex in your project. You can expand this application by adding more functionality or breaking it down into modules for better scalability.

Advanced Vuex Features

Vuex Modules: Organizing State in Large Applications

As we delve into the more advanced capabilities of Vuex, we can't overlook the important concept of modules. Vuex modules allow you to structure your application's state in a way that remains manageable, even as your project grows and becomes more complex. Imagine a vast palace with numerous rooms and corridors; without clear organization, it would be easy to get lost. Similarly, in large applications, modules help us avoid chaos and maintain order.

When your application scales significantly, state management can become challenging. Using a single store for all state can lead to inconveniences: the code becomes less readable, errors become harder to debug, and adding new features becomes more difficult. Modules come to the rescue by providing a way to divide the state into logical blocks.

Let's consider an example of creating modules in Vuex in practice. We'll split our state into two modules: user and tasks, each responsible for its own data domain.

Let's start by creating files for our modules.

Adding a Mock API

// src/api/mockApi.js

export const api = {
  async getUser() {
    return {
      id: 1,
      name: 'John Doe',
      email: 'john.doe@example.com'
    };
  }
};

User Module (user.js):

// store/modules/user.js

import { api } from '../../api/mockApi';

const state = {
  user: null,
};

const getters = {
  isLoggedIn(state) {
    return !!state.user;
  },
  getUser(state) {
    return state.user;
  }
};

const mutations = {
  setUser(state, user) {
    state.user = user;
  }
};

const actions = {
  async fetchUser({ commit }) {
    const user = await api.getUser();
    commit('setUser', user);
  }
};

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
};

Модуль задач (tasks.js):

// store/modules/tasks.js

const state = {
  tasks: []
};

const getters = {
  allTasks: (state) => state.tasks,
};

const mutations = {
  addTask(state, task) {
    state.tasks.push(task);
  },
  
  removeTask(state, taskIndex) { 
    state.tasks.splice(taskIndex,1); 
 } 
}; 

const actions= { 
  async addNewTask({commit},task){ 
    // Здесь можно добавить асинхронный код для взаимодействия с API 
    commit('addTask',task);  
  }, 
    
  async deleteTask({commit},taskIndex){ 
    // Здесь можно добавить асинхронный код для взаимодействия с API 
    commit('removeTask',taskIndex);  
  } 
}; 

export default { 
  namespaced:true ,  
  state ,  
  getters ,  
  mutations ,   
  actions     
};

Combining Our Modules in the Main Store File

Now let's combine our modules in the main store file.

// store/index.js

import { createStore } from 'vuex';
import user from './modules/user';
import tasks from './modules/tasks';

export default createStore({
  modules:{    
    user ,    
    tasks      
  }   
});

 To use data from our modules in components, we need to slightly adjust our approach to calling getters and actions.

Example of Using Modules in a ComponentTodoList.vue:

<template>
  <div>
    <h1>Список задач</h1>
    <ul>
      <li v-for="(task, index) in allTasks" :key="index">
        {{ task }}
        <button @click="removeTask(index)">Удалить</button>
      </li>
    </ul>
    <input v-model="newTask" placeholder="Добавить новую задачу" />
    <button @click="addTask">Добавить</button>
  </div>
</template>

<script>
  import { mapGetters,mapActions}from'vuex'; 

  export default{ data(){ return{ newTask:'' };},
  methods:{
    ...mapActions('tasks',['addNewTask','deleteTask']), 
    
    addTask() {
      if (this.newTask.trim()) {
        this.addNewTask(this.newTask);
        this.newTask = '';
      }
    },

    removeTask(index) {
      this.deleteTask(index);
    }
  }, 
 
 computed:{
 ...mapGetters('tasks',['allTasks'])}
 };
 </script>

<style scoped>
/* Ваши стили */
</style>

As you can see, we use namespacing to access the getters and actions of a specific module through its name (tasks or user). This allows us to clearly separate the responsibilities of each module.

Vuex modules help break down code into logical parts, making it more readable and easier to maintain. Now that you know the basics of working with Vuex modules, you can apply them in your projects to achieve better scalability and structure.

 

Asynchronous Actions: Data Handling and API Interaction

In an era where interaction with external APIs and asynchronous operations has become an integral part of web application development, understanding how to work with asynchronous actions in Vuex takes center stage. Asynchronous actions allow us to manage the application's state in response to data received from external sources. Imagine a protagonist in a novel who embarks on a journey to gain new knowledge and returns to share it with the community. Similarly, our application requests data from the server and updates the state based on the responses received.

Asynchronous actions in Vuex play a key role when working with APIs. Unlike mutations, which modify the state synchronously, actions can include asynchronous code and commit mutations upon completing operations. This allows us to effectively manage the state when interacting with external services.

Let's consider an example of a simple task management application that fetches a list of tasks from an API and updates the state. We'll use the tasks module to manage this data.

Mocking API Requests

Let's create a mocked API that will be used in Vuex actions. This will allow you to test the application without needing a real server.

// src/api/mockApi.js

export const api = {
  async getTasks() {
    // Замокированный ответ от API
    return {
      data: [
        'Задача 1',
        'Задача 2',
        'Задача 3'
      ]
    };
  },
  
  async addTask(task) {
    // Мокирование добавления задачи
    console.log(`Задача "${task}" добавлена.`);
  },

  async deleteTask(taskIndex) {
    // Мокирование удаления задачи
    console.log(`Задача с индексом ${taskIndex} удалена.`);
  }
};

Now we'll create actions to make API requests.

// store/modules/tasks.js

import { api } from '../../api/mockApi'; // Импортируем мокированный API

const state = {
  tasks: []
};

const getters = {
  allTasks: (state) => state.tasks,
};

const mutations = {
  setTasks(state, tasks) {
    state.tasks = tasks;
  },
  addTask(state, task) {
    state.tasks.push(task);
  },
  removeTask(state, taskIndex) {
    state.tasks.splice(taskIndex, 1);
  }
};

const actions = {
  async fetchTasks({ commit }) {
    try {
      const response = await api.getTasks(); // Запрос к мокированному API
      commit('setTasks', response.data); // Обновляем состояние задач
    } catch (error) {
      console.error("Ошибка при получении задач:", error);
    }
  },
  
  async addNewTask({ commit }, task) { 
    try { 
      await api.addTask(task); 
      commit('addTask', task); 
    } catch (error) { 
      console.error("Ошибка при добавлении задачи:", error); 
    }  
  },

  async deleteTask({ commit }, taskIndex) {  
    try {    
      await api.deleteTask(taskIndex);    
      commit('removeTask', taskIndex);   
    } catch (error) {     
      console.error("Ошибка при удалении задачи:", error);   
    }  
  } 
}; 

export default {  
  namespaced: true,   
  state,   
  getters,   
  mutations,    
  actions      
};

In this example, we have defined three main actions:

  1. fetchTasks: Asynchronously retrieves the list of tasks from the API and commits the setTasks mutation to update the state.
  2. addNewTask: Sends a new task to the server via the API and commits the addTask mutation after the request is successfully completed.
  3. deleteTask: Deletes a task by its index by calling the corresponding API method and commits the removeTask mutation.

Now let's look at how to use these actions in our components.

Example of Using Actions in a Component TodoList.vue:

<template>
  <div>
    <h1>Список задач</h1>
    
    <!-- Кнопка для получения задач из API -->
    <button @click="fetchTasks">Загрузить задачи</button>

    <ul>
      <li v-for="(task, index) in allTasks" :key="index">
        {{ task }}
        <button @click="deleteExistingTask(index)">Удалить</button>
      </li>
    </ul>
    
    <input v-model="newTask" placeholder="Добавить новую задачу" />
    <button @click="addTask">Добавить</button>
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex';

export default {
  data() {
    return {
      newTask: ''
    };
  },
  
  methods: {
    ...mapActions('tasks', ['fetchTasks', 'addNewTask', 'deleteTask']), // Подключаем действия для работы с API

    addTask() {
      if (this.newTask.trim()) {
        this.addNewTask(this.newTask);
        this.newTask = '';
      }
    },

    deleteExistingTask(index) {
      this.deleteTask(index);
    }
  },
 
  computed: {
    ...mapGetters('tasks', ['allTasks']) // Подключаем геттер для получения задач из хранилища Vuex
  }
};
</script>

<style scoped>
/* Ваши стили */
</style>

In the component, we use methods from the tasks module, such as fetchTasks, to load tasks when the component is mounted, as well as methods for adding and deleting tasks.

Asynchronous actions in Vuex provide a tool for working with data from external services. They allow us to handle API requests without blocking the user interface, ensuring smooth and responsive user interaction with the application.

Continue experimenting with different scenarios for using actions in your projects—this will help you gain a deeper understanding of Vuex's capabilities and improve your skills in working with asynchronous code. Enjoy your journey to mastery!

 

Best Practices for State Management in Large Projects

In the previous chapters, we explored the basics of using modules and asynchronous actions in Vuex. Now it's time to delve into best practices that will help you effectively organize your application's state and avoid common mistakes.

Using Constants: This helps prevent errors related to typos and makes the code more explicit and easier to refactor. Let's create a types.js file where all mutation and action types will be stored.

// store/types.js
export const SET_USER = 'SET_USER';
export const FETCH_USER = 'FETCH_USER';

export const SET_TASKS = 'SET_TASKS';
export const ADD_TASK = 'ADD_TASK';
export const REMOVE_TASK = 'REMOVE_TASK';
export const FETCH_TASKS = 'FETCH_TASKS';

Updating the user.js Module to Use Constants for Mutations and Actions

// store/modules/user.js
import { SET_USER, FETCH_USER } from '../types';
import api from '../../services/api'; // Импортируем API сервис

export default {
  namespaced: true,
  state: () => ({
    user: null,
  }),
  getters: {
    isLoggedIn(state) {
      return !!state.user;
    },
    getUser(state) {
      return state.user;
    }
  },
  mutations: {
    [SET_USER](state, user) {
      state.user = user;
    }
  },
  actions: {
    async [FETCH_USER]({ commit }) {
      try {
        const response = await api.getUser();
        commit(SET_USER, response.data);
      } catch (error) {
        console.error("Ошибка при получении пользователя:", error);
      }
    }
  }
};

Updating the tasks.js Module to Use Constants and Extract API Requests into a Separate Service

// store/modules/tasks.js
import { SET_TASKS, ADD_TASK, REMOVE_TASK, FETCH_TASKS } from '../types';
import api from '../../services/api'; // Импортируем API сервис

const state = {
  tasks: []
};

const getters = {
  allTasks: (state) => state.tasks,
};

const mutations = {
  [SET_TASKS](state, tasks) {
    state.tasks = tasks;
  },
  [ADD_TASK](state, task) {
    state.tasks.push(task);
  },
  [REMOVE_TASK](state, taskIndex) {
    state.tasks.splice(taskIndex, 1);
  }
};

const actions = {
  async [FETCH_TASKS]({ commit }) {
    try {
      const response = await api.getTasks();
      commit(SET_TASKS, response.data);
    } catch (error) {
      console.error("Ошибка при получении задач:", error);
    }
  },
  
  async addNewTask({ commit }, task) { 
    try { 
      await api.addTask(task); 
      commit(ADD_TASK, task); 
    } catch (error) { 
      console.error("Ошибка при добавлении задачи:", error); 
    }  
  },

  async deleteTask({ commit }, taskIndex) {  
    try {    
      await api.deleteTask(taskIndex);    
      commit(REMOVE_TASK, taskIndex);   
    } catch (error) {     
      console.error("Ошибка при удалении задачи:", error);   
    }  
  }
};

export default {  
  namespaced: true,   
  state,   
  getters,   
  mutations,    
  actions      
};

Adding axios to Our Project

npm install axios

Moving All API Interactions into a Separate File api.js. This improves the testability and structure of the code.

// services/api.js
import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 1000,
  headers: { 'X-Custom-Header': 'foobar' }
});

export default {
  getUser() {
    return apiClient.get('/user');
  },
  getTasks() {
    return apiClient.get('/tasks');
  },
  addTask(task) {
    return apiClient.post('/tasks', { task });
  },
  deleteTask(taskIndex) {
    return apiClient.delete(`/tasks/${taskIndex}`);
  }
};

Installing and Configuring Vue Router

Why Do You Need Vue Router? Key Features

Imagine reading a classic novel where each chapter flows seamlessly into the next, and the plot develops sequentially and logically. Similarly, in a web application, users expect smooth and intuitive navigation between different parts of the interface. Without a proper routing tool, this can turn into chaos.

Vue Router helps structure your application so that each component can be associated with a specific URL. This makes it possible to:

  • Create User-Friendly Navigation: Users can move between pages of the application as effortlessly as flipping through chapters in a book.
  • Support Browser History: By leveraging browser history features like the "back" and "forward" buttons, users can easily return to previous states of the application.
  • Dynamic Component Loading: You can load components only when they are actually needed by the user, which improves application performance.
  • Flexibility in Development: The router allows you to break your application into logical parts (pages or sections), simplifying development and testing.

Installing and Configuring Vue Router

Like a skilled guide blazing a trail through a dense forest, we'll walk step by step through all the necessary stages to integrate this powerful routing library into your project.

To start using Vue Router in your Vue project, you'll need to perform a few simple steps. We'll go through the installation process and basic setup so you can quickly integrate this powerful tool into your application.

Step 1: Install Vue Router

First, you need to install the vue-router package. This can be done using npm or yarn.

npm install vue-router@next --save

Or Using Yarn:

// router/index.js

import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../components/HomeView.vue';
import AboutView from '../components/AboutView.vue';

const routes = [
  { path: '/', component: HomeView },
  { path: '/about', component: AboutView }
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

In this file, we import the necessary functions from vue-router, as well as the components that will be displayed for the corresponding routes. Then, we define the routes array, where each route is linked to a component. At the end, we create a router instance and export it.

Step 3: Connecting the Router to the Application

Now, we need to connect our router to the main application. Open the main.js file and add the import and usage of the router as follows:

// main.js

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

createApp(App)
  .use(router)
  .mount('#app');

In this file, we import the previously created router and register it using the .use() method. After that, we mount our application to the element with the ID #app.

Step 4: Adding Components for Routes

Next, we'll create the components that will be displayed for different routes. For example, let's create two simple components — HomeView and AboutView:

<!-- components/HomeView.vue -->
<template>
  <div>
    <h1>Home Page</h1>
    <p>Добро пожаловать на главную страницу!</p>
  </div>
</template>

<script>
export default {
  name: 'HomeView'
};
</script>

<style scoped>
/* Ваши стили */
</style>
<!-- components/AboutView.vue -->
<template>
  <div>
    <h1>About Page</h1>
    <p>Это страница "О нас".</p>
  </div>
</template>

<script>
export default {
  name: 'AboutView'
};
</script>

<style scoped>
/* Ваши стили */
</style>

Step 5: Setting Up Navigation Between Pages

To navigate between routes, use the <router-link> component and <router-view> to display the content of the current route:

<!-- App.vue -->
<template>
  <div id="app">
    <nav>
      <router-link to="/">Home</router-link> 
      <router-link to="/about">About</router-link>
    </nav> 
    <router-view></router-view>
  </div>
</template>

<script>
// import необходимых компонентов...
export default {
 // Логика компонента...
};
</script>

Here, we've created a simple navigation menu using <router-link> to navigate between the "Home" and "About" pages. <router-view> serves as a container for displaying the active component based on the current route.

Creating and Setting Up Simple Routes

Like a skilled artist painting on a canvas with their brush, we continue our journey of creating and setting up routes in a Vue application.

Let's start by creating the basic routes for our application. Imagine your application is a book with several chapters, each having its unique page. To create such pages, you need to set up the corresponding routes.

We begin by defining the basic routes. We've already seen an example of configuring the router/index.js file, where two simple routes are defined: HomeView and AboutView.

Sometimes, it's necessary to create a route that accepts parameters. For example, a user's profile page may depend on the user's identifier in the URL. To achieve this, dynamic segments are used:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../components/HomeView.vue';
import AboutView from '../components/AboutView.vue';
import UserProfileView from '../components/UserProfileView.vue';

const routes = [
  { path: '/', name: 'home', component: HomeView },
  { path: '/about', name: 'about', component: AboutView },
  { path: '/user/:id', name: 'user', component: UserProfileView }, 
  { path: '/:catchAll(.*)', redirect: '/' } // Маршрут-заглушка для неизвестных путей
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

In this example, we added the /user/:id route, where :id is a dynamic segment. The UserProfileView component will receive this parameter through the $route.params object.

Now, let's create the UserProfileView component to display user information based on their ID. To make the example more realistic, we'll mock some simple data:

<!-- components/UserProfileView.vue -->
<template>
  <div>
    <h1>User Profile</h1>
    <p>User ID: {{ userId }}</p>
    <p>User Name: {{ userName }}</p>
  </div>
</template>

<script>
export default {
  name: 'UserProfileView',
  computed: {
    userId() {
      return this.$route.params.id;
    },
    userName() {
      // Мок-данные: Простое сопоставление ID с именем
      const userNames = {
        '123': 'John Doe',
        '456': 'Jane Smith',
        '789': 'Alice Johnson'
      };
      return userNames[this.userId] || 'Unknown User';
    }
  }
};
</script>

<style scoped>
/* Ваши стили */
</style>

In this component, we retrieve the id parameter from the $route.params object and display it on the page.

To navigate between these routes, use the <router-link> component:

<!-- App.vue -->
<template>
  <div id="app">
    <nav>
      <router-link to="/">Home</router-link> 
      <router-link to="/about">About</router-link>
      <router-link :to="{ name: 'user', params: { id: '123' } }">User Profile 123</router-link> 
      <router-link :to="{ name: 'user', params: { id: '456' } }">User Profile 456</router-link> 
      <router-link :to="{ name: 'user', params: { id: '789' } }">User Profile 789</router-link>
    </nav> 
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'App'
};
</script>

<style scoped>
/* Ваши стили */
</style>

Here, we've added a link to the user profile with ID 123, using the params property to pass a dynamic parameter.

Handling Navigation Errors: Anticipating Obstacles

Just as a wise strategist anticipates obstacles, we must plan for error handling during navigation. For example, if a user enters an incorrect path to one of the sub-routes, we can create a fallback route:

{ path: '/:catchAll(.*)', redirect: '/' }

This ensures that users do not land on non-existent pages.

These simple steps will help you set up both basic and dynamic routes in your Vue application. Just as expertly applied brushstrokes create a beautiful canvas, proper routing configuration ensures smooth and logical navigation for users throughout your application.

Advanced Routing

Nested Routes: Building Multi-Level Navigation

Like a great architect constructing majestic palaces and cathedrals, we continue our journey into the world of Vue Router. This time, we'll focus on creating nested routes, which allow us to implement multi-level navigation within our application.

Nested routes enable you to create a page structure where one page serves as a parent to others. This is especially useful for large applications that require clear organization of the interface and logic. For example, an administrative panel might contain various sections: Users, Settings, and Reports.

Let's begin by setting up routes for our administrative panel and nested pages such as "Users" and "Settings." We'll update the router/index.js file accordingly.

// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../components/HomeView.vue';
import AdminView from '../components/AdminView.vue';
import UsersView from '../components/UsersView.vue';
import SettingsView from '../components/SettingsView.vue';
import UserProfileView from '../components/UserProfileView.vue';

const routes = [
  { path: '/', name: 'home', component: HomeView },
  {
    path: '/admin',
    name: 'admin',
    component: AdminView,
    children: [
      { path: 'users', name: 'users', component: UsersView },
      { path: 'settings', name: 'settings', component: SettingsView },
      { path: 'user/:id', name: 'userProfile', component: UserProfileView }
    ]
  },
  { path: '/:catchAll(.*)', redirect: '/' }
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

Here, we'll create the necessary components for our administrative panel and its nested routes.

The AdminView component will contain the navigation for the administrative panel and a <router-view> to display the child routes.

<!-- components/AdminView.vue -->
<template>
  <div>
    <h1>Административная панель</h1>
    <nav>
      <router-link to="/admin/users">Пользователи</router-link>
      <router-link to="/admin/settings">Настройки</router-link>
    </nav>
    <router-view></router-view> <!-- Здесь будут отображаться дочерние компоненты -->
  </div>
</template>

<script>
export default {
  name: 'AdminView'
};
</script>

<style scoped>
/* Ваши стили */
</style>

The UsersView component will display a list of users. To accomplish this, you can use mock data.

<!-- components/UsersView.vue -->
<template>
  <div>
    <h2>Пользователи</h2>
    <ul>
      <li v-for="user in users" :key="user.id">
        <router-link :to="{ name: 'userProfile', params: { id: user.id }}">{{ user.name }}</router-link>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'UsersView',
  data() {
    return {
      users: [
        { id: '123', name: 'John Doe' },
        { id: '456', name: 'Jane Smith' },
        { id: '789', name: 'Alice Johnson' }
      ]
    };
  }
};
</script>

<style scoped>
/* Ваши стили */
</style>

In this component, the UsersView will display a list of users. To accomplish this, you can use mock data.

<!-- components/SettingsView.vue -->
<template>
  <div>
    <h2>Настройки</h2>
    <!-- Логика отображения настроек -->
  </div>
</template>

<script>
export default {
  name: 'SettingsView'
};
</script>

<style scoped>
/* Ваши стили */
</style>

The UserProfileView component will be used to display a user's profile based on their ID.

<!-- components/UserProfileView.vue -->
<template>
  <div>
    <h1>Профиль пользователя</h1>
    <p>User ID: {{ userId }}</p>
    <p>User Name: {{ userName }}</p>
  </div>
</template>

<script>
export default {
  name: 'UserProfileView',
  computed: {
    userId() {
      return this.$route.params.id;
    },
    userName() {
      const userNames = {
        '123': 'John Doe',
        '456': 'Jane Smith',
        '789': 'Alice Johnson'
      };
      return userNames[this.userId] || 'Unknown User';
    }
  }
};
</script>

<style scoped>
/* Ваши стили */
</style>

Now, let's update the App.vue component to add links to the Administrative Panel and the Home page:

<!-- App.vue -->
<template>
  <div id="app">
    <nav>
      <router-link to="/">Home</router-link>
      <router-link to="/admin">Admin Panel</router-link>
    </nav>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'App'
};
</script>

<style scoped>
/* Ваши стили */
</style>

Now you have a working example of setting up nested routes. We've created an administrative panel with nested routes for managing users and settings, as well as added support for dynamic routes for user profiles. All components are interconnected through routes, and you can use mock data for testing and debugging. This structure will help you implement complex and multi-level navigation in your application, ensuring flexibility and scalability.

Protecting Routes: Authentication and Authorization

In the vast ocean of modern web application possibilities, just as the sea requires lighthouses for safe navigation, complex routes need protection to ensure security and manage access. In Vue 3, we can use authentication and authorization to restrict access to certain pages, ensuring that only authorized users can access them. Let's dive into these depths to understand how to protect routes in your application.

Protecting routes involves verifying whether a user has the rights to access a specific page before rendering it. This is achieved using navigation guards, which allow you to perform checks before transitioning to a new route.

// services/auth.js
export default {
  isAuthenticated() {
    // Простая проверка аутентификации пользователя (например, наличие токена)
    return !!localStorage.getItem('userToken');
  },
  login(token, roles = []) {
    localStorage.setItem('userToken', token);
    localStorage.setItem('userRoles', JSON.stringify(roles));
  },
  logout() {
    localStorage.removeItem('userToken');
    localStorage.removeItem('userRoles');
  },
  hasRole(role) {
    const userRoles = JSON.parse(localStorage.getItem('userRoles')) || [];
    return userRoles.includes(role);
  }
};

This service provides methods for checking the authentication state (isAuthenticated), logging in (login), and logging out (logout) the user. Additionally, a role verification method (hasRole) has been added to check the user's roles.

Now, using navigation guards, we'll protect the routes. We'll add a global guard to our router to verify user access before navigating to protected routes.

// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import AdminView from '../components/AdminView.vue';
import HomeView from '../components/HomeView.vue';
import LoginView from '../components/LoginView.vue';
import SettingsView from '../components/SettingsView.vue';
import UnauthorizedView from '../components/UnauthorizedView.vue';
import UserProfileView from '../components/UserProfileView.vue';
import UsersView from '../components/UsersView.vue';
import auth from '../services/auth';

const routes = [
  { path: '/', name: 'home', component: HomeView },
  { path: '/login', name: 'login', component: LoginView },
  { path: '/unauthorized', name: 'unauthorized', component: UnauthorizedView },
  {
    path: '/admin',
    name: 'admin',
    component: AdminView,
    meta: { requiresAuth: true, roles: ['admin'] }, // Маршрут требует аутентификации и роли "admin"
    children: [
      { path: 'users', name: 'users', component: UsersView },
      { path: 'settings', name: 'settings', component: SettingsView },
      { path: 'user/:id', name: 'userProfile', component: UserProfileView }
    ]
  },
  { path: '/:catchAll(.*)', redirect: '/' } // Маршрут-заглушка для неизвестных путей
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

// Глобальный навигационный охранник
router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!auth.isAuthenticated()) {
      next({ name: 'login' });
    } else if (to.meta.roles && !auth.hasRole(to.meta.roles[0])) {
      next({ name: 'unauthorized' }); // Перенаправление на страницу "Доступ запрещён"
    } else {
      next();
    }
  } else {
    next();
  }
});

export default router;
  • The /admin route requires user authentication and an admin role.
  • A global navigation guard checks whether the user is authenticated and possesses the necessary roles. If not, the user is redirected to the login page or to an "Access Denied" page.

    Let's create the LoginView component that will manage the user login process:

<!-- components/LoginView.vue -->
<template>
  <div>
    <h1>Вход</h1>
    <input v-model="token" placeholder="Введите ваш токен" />
    <button @click="login">Войти</button>
  </div>
</template>

<script>
import auth from '../services/auth';

export default {
  data() {
    return {
      token: ''
    };
  },
  methods: {
    login() {
      // В реальном приложении вы бы получили токен и роли от сервера после успешного входа.
      auth.login(this.token, ['admin']); // Мокаем пользователя с ролью admin
      this.$router.push('/admin'); // Перенаправление на защищённый маршрут после входа
    }
  }
};
</script>

<style scoped>
/* Ваши стили */
</style>

Thought for 5 seconds

The LoginView component allows users to enter a token and log in. After a successful login, the user is redirected to the protected /admin route.

Let's create the UnauthorizedView component, which will be displayed when attempting to access a route without the necessary permissions:

<!-- components/UnauthorizedView.vue -->
<template>
  <div>
    <h1>Доступ запрещён</h1>
    <p>У вас нет прав для доступа к этой странице.</p>
  </div>
</template>

<script>
export default {
  name: 'UnauthorizedView'
};
</script>

<style scoped>
/* Ваши стили */
</style>

This component will be displayed if a user attempts to access a route without the necessary permissions.

Now you have a working example of how to protect routes in a Vue application using authentication and authorization. You've created an authentication service, configured routes with access checks, and added global navigation guards to manage access based on user roles. This approach ensures the security of your application and the proper distribution of access among users.

Optimizing Routing and Enhancing User Experience (UX)

In the vast landscape of modern web applications, every user interaction with page navigation should be smooth and seamless, much like the flow of a river. Optimizing routing and improving user experience (UX) are critically important to achieving this goal. In this section, we will explore several methods that will help you make your Vue 3 application faster and more user-friendly.

One of the most effective optimization techniques is lazy loading components. This approach allows components to be loaded only when they are actually needed by the user. It reduces the initial bundle size and accelerates the application's initial load time.

// router/index.js
import { createRouter, createWebHistory } from 'vue-router';

// Ленивая загрузка компонентов
const HomeView = () => import('../components/HomeView.vue');
const AboutView = () => import('../components/AboutView.vue');
const UserProfileView = () => import('../components/UserProfileView.vue');

const routes = [
  { path: '/', name: 'home', component: HomeView },
  { path: '/about', name: 'about', component: AboutView },
  {
    path: '/user/:id',
    name: 'userProfile',
    component: UserProfileView,
    beforeEnter: async (to, from, next) => {
      // Предзагрузка данных пользователя перед рендерингом
      await store.dispatch('fetchUserData', to.params.id);
      next();
    }
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

In this example, the Home and About components will be loaded only when navigating to their respective routes.

To enhance the User Experience (UX), it's crucial to minimize delays in data rendering. Utilizing navigation guards and component lifecycle methods allows you to preload the necessary data before the page is rendered.

// router/index.js
import store from '../store';

const routes = [
  // другие маршруты...
  {
    path: '/user/:id',
    name: 'userProfile',
    component: () => import('../components/UserProfileView.vue'),
    beforeEnter: async (to, from, next) => {
      await store.dispatch('fetchUserData', to.params.id);
      next();
    }
  }
];

export default router;

Here, before rendering the UserProfileView component, user data is preloaded using the fetchUserData method. This allows information to be displayed immediately after navigating to the page.

Data caching, especially for frequently visited pages, can significantly enhance application performance. In Vue, you can use the vuex-persistedstate plugin to preserve state between user sessions.

// store/index.js
import { createStore } from 'vuex';
import createPersistedState from 'vuex-persistedstate';
import api from '../api'; // Предполагаемый API модуль

const store = createStore({
  state() {
    return {
      userData: null,
    };
  },
  mutations: {
    setUserData(state, userData) {
      state.userData = userData;
    }
  },
  actions: {
    async fetchUserData({ commit }, userId) {
      const response = await api.getUser(userId);
      commit('setUserData', response.data);
    }
  },
  plugins: [createPersistedState()] // Плагин для сохранения состояния между сессиями
});

export default store;

In this example, the user data loaded via fetchUserData is stored in local storage using the vuex-persistedstate plugin, allowing the state to persist across sessions.

To make the user experience smoother and more enjoyable, you can add route transition animations and loading indicators.

<!-- App.vue -->
<template>
  <div id="app">
    <nav>
      <router-link to="/">Home</router-link>
      <router-link to="/about">About</router-link>
    </nav>

    <!-- Анимация переходов между маршрутами -->
    <transition name="fade" mode="out-in">
      <router-view></router-view>
    </transition>
  </div>
</template>

<script>
export default {
  name: 'App'
};
</script>

<style scoped>
/* Анимация переходов между маршрутами */
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.5s;
}

.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

Adding animations makes transitions between pages smoother and more pleasant for the user. It is also worth adding loading indicators (for example, spinners) in case of long-term operations.

By lazily loading components, preloading data before rendering, caching state, and improving UX with animations and loading indication, you can significantly improve the performance of your Vue application and provide users with the smoothest and most enjoyable experience.

ChatGPT
Eva
💫 Eva assistant