# Component Design Standards for Vue.js
This document outlines the coding standards specifically focused on *Component Design* for Vue.js applications. Following these guidelines will help ensure that your Vue.js components are reusable, maintainable, performant, and secure. These recommendations are based on current best practices in the Vue.js ecosystem and reference the latest version of Vue.js (currently Vue 3).
## 1. Component Architecture and Organization
### 1.1 Single Responsibility Principle (SRP)
**Standard:** Each component should have a single, well-defined responsibility.
**Why:** SRP increases readability, testability, and reusability. A component focused on a single task is easier to understand, test in isolation, and reuse in different parts of the application.
**Do This:** Break down complex features into smaller, single-purpose components. Use composition API to manage complex logic within these components.
**Don't Do This:** Create "God components" that handle multiple unrelated tasks. This leads to tightly coupled code that's difficult to maintain and reuse.
**Example:**
**Good:** Separate components for displaying product information and handling add-to-cart functionality.
"""vue
// ProductInfo.vue
"""
"""vue
// AddToCartButton.vue
"""
**Bad:** A single component handling both product display and cart functionality.
"""vue
// Product.vue (Anti-pattern: God Component)
"""
### 1.2 Component Naming
**Standard:** Use PascalCase for component names. Component files should match the component name.
**Why:** PascalCase clearly distinguishes Vue components from native HTML elements in templates, enhancing readability. Consistent file naming makes it easier to locate components within the project.
**Do This:** Name components descriptively using PascalCase. e.g., "MyComponent.vue", "UserProfile.vue".
**Don't Do This:** Use kebab-case or snake_case for component names, as it reduces readability within templates.
**Example:**
"""
components/
├── MyButton.vue
├── UserProfile.vue
"""
"""vue
// MyButton.vue
"""
### 1.3 Component Directory Structure
**Standard:** Organize components into a logical directory structure based on feature, module, or component type (e.g., "components/", "views/", "pages/").
**Why:** A well-organized directory structure improves maintainability and makes it easier to navigate the codebase, especially in large projects.
**Do This:** Group related components into subdirectories. Use "index.js" (or "index.ts") files to export components from a directory for easier imports.
**Don't Do This:** Place all components in a single directory, as this becomes unwieldy as the project grows.
**Example:**
"""
src/
├── components/
│ ├── Button/
│ │ ├── Button.vue
│ │ └── index.js
│ ├── Input/
│ │ ├── Input.vue
│ │ └── index.js
"""
"""javascript
// src/components/Button/index.js
import Button from './Button.vue';
export { Button };
"""
### 1.4 Use of Global Components (Judiciously)
**Standard:** Register components globally only when they are used across a large number of pages and are truly foundational (Utility components).
**Why:** Global registration simplifies usage in templates, but excessive use can lead to larger bundle sizes and naming conflicts.
**Do This:** Register commonly used utility components globally (e.g., a custom button component). Use local registration (importing components directly into the components that use them) for more specific components.
**Don't Do This:** Globally register every component in the application, as this can negatively impact performance.
**Example:**
"""javascript
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import MyButton from './components/MyButton.vue';
const app = createApp(App);
app.component('MyButton', MyButton); // Global registration
app.mount('#app');
"""
## 2. Component Props and Emits
### 2.1 Prop Definition
**Standard:** Define props explicitly with type validation, required status, and default values where appropriate.
**Why:** Explicit prop definitions improve code clarity, prevent runtime errors, and provide better documentation.
**Do This:** Use the "defineProps" macro in "
"""
### 2.2 Prop Naming Conventions
**Standard:** Use camelCase for prop names in the component definition and kebab-case when passing prop values in the template.
**Why:** This convention aligns with JavaScript (camelCase) and HTML (kebab-case) standards, improving consistency and readability.
**Do This:** Define props as "myPropName" in the component, and pass them as "my-prop-name" in the template.
**Don't Do This:** Use inconsistent casing for props, which can lead to confusion and errors.
**Example:**
"""vue
// MyComponent.vue
"""
"""vue
// ParentComponent.vue
"""
### 2.3 Emitting Events
**Standard:** Define emitted events using "defineEmits" for clarity and type safety. Use descriptive event names.
**Why:** Explicit event definitions improve code readability and help prevent errors related to event handling. Descriptive event names improve communication and maintainability.
**Do This:** Use "defineEmits" in "
"""
"""vue
// ParentComponent.vue
"""
### 2.4 Avoid Prop Mutation
**Standard:** Treat props as read-only within the component. If you need to modify a prop's value, create a local data property.
**Why:** Mutating props directly violates the one-way data flow principle of Vue.js, making it harder to track state changes and debug issues.
**Do This:** Create a computed property or reactive variable to hold the local, mutable value. Emit an event to signal that an external update is needed.
**Don't Do This:** Directly modify the value of a prop within the component.
**Example:**
**Good:**
"""vue
// MyComponent.vue
"""
**Bad:**
"""vue
// MyComponent.vue (Anti-pattern: Prop mutation)
"""
## 3. Component Communication
### 3.1 Props and Events for Parent-Child Communication
**Standard:** Use props to pass data down from parent to child components, and events to communicate actions or state changes from child to parent components.
**Why:** This unidirectional data flow makes state management predictable and easier to understand.
**Do This:** Adhere to the props-down, events-up pattern.
**Don't Do This:** Directly access or mutate parent component state from a child component without using events, or rely on global state for simple parent-child interactions.
**Example:** (See examples in Props and Emits sections above.)
### 3.2 Provide / Inject
**Standard:** Use "provide" and "inject" for passing data *down* the component tree, especially when props become cumbersome or when dealing with deeply nested components. Use with caution to avoid tight coupling.
**Why:** "provide"/"inject" allows data to be shared across multiple levels of the component tree without needing prop drilling.
**Caveat:** Heavy use of "provide/inject" can create implicit dependencies and make components harder to reason about. Use it judiciously.
**Do This:** Use "provide" in a parent component to make data or methods available to its descendants. Use "inject" in descendant components to access the provided data or methods. Strongly consider Symbols as keys to avoid naming collisions.
**Don't Do This:** Overuse "provide"/"inject" for simple parent-child communication, or use it to modify state directly without a clear pattern, as this makes tracking of state changes nearly impossible.
**Example:**
"""vue
// MyProvider.vue (Parent Component - Providing)
"""
"""vue
// MyConsumer.vue (Child Component - Injecting)
"""
Consider using Symbols for keys to prevent collisions:
"""vue
// Provider
"""
### 3.3 State Management (Vuex or Pinia)
**Standard:** Use a state management library (Vuex or Pinia) for complex application state that needs to be shared between many components or persisted across routes. *Pinia is now the recommended state store*.
**Why:** State management libraries provide a centralized store for application state, making it easier to manage, track changes, and ensure consistency across components.
**Do This:** Structure your state logically into modules (or stores in Pinia). Use actions to commit mutations, and use getters to derive computed state. Follow Pinia's composition API style.
**Don't Do This:** Use local component state for data that needs to be shared globally, or directly mutate the state without using actions.
**Example (Pinia):**
"""javascript
// stores/counter.js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++;
},
decrement() {
this.count--;
}
}
});
"""
"""vue
// MyComponent.vue
"""
## 4. Component Logic and Composition
### 4.1 Composition API
**Standard:** Favor the Composition API over the Options API for new components. The Options API is still valuable for simpler components or when migrating existing code.
**Why:** The Composition API offers superior code organization, reusability through composables, and better TypeScript support.
**Do This:** Use "
"""
### 4.2 Reusability with Composables
**Standard:** Extract reusable component logic into composable functions ("composables").
**Why:** Composables promote code reuse, improve testability, and help maintain a DRY (Don't Repeat Yourself) codebase.
**Do This:** Create composables for tasks such as data fetching, form validation, and managing external services. Prefix composable names with "use" (e.g., "useFetch", "useForm"). Return reactive state, computed properties, and functions from the composable.
**Don't Do This:** Duplicate logic across multiple components. Place component-specific logic within composables, reducing their reusability. Name composables poorly.
**Example:** (See example in Composition API section above.)
### 4.3 Computed Properties
**Standard:** Use computed properties for deriving values from reactive state.
**Why:** Computed properties are cached and only re-evaluated when their dependencies change, improving performance. They also provide a cleaner and more declarative way to express derived state.
**Do This:** Use "computed" in "
"""
### 4.4 Watchers
**Standard:** Use watchers sparingly, generally only when you need to react to specific state changes and perform side effects (e.g., logging, calling an external API). Prefer computed properties for deriving state.
**Why:** Watchers can make code harder to reason about because their execution is implicit. Computed properties are usually a better choice for deriving values.
**Do This:** Use "watch" (or "watchEffect" when you don't need access to the previous value) in "
"""
## 5. Component Styling
### 5.1 Scoped Styles
**Standard:** Use scoped styles in most components to encapsulate styles and prevent conflicts.
**Why:** Scoped styles ensure that a component's styles only apply to that component's template, making it easier to maintain and reuse components.
**Do This:** Add the "scoped" attribute to the "
"""
### 5.2 CSS Preprocessors
**Standard:** Use CSS preprocessors (e.g., Sass, Less, Stylus) to enhance styling with features like variables, mixins, and nesting.
**Why:** CSS preprocessors improve code organization, reusability, and maintainability.
**Do This:** Configure your build system to support your chosen CSS preprocessor. Use preprocessor features to write more concise and maintainable styles. Choose one preprocessor and stick to it.
**Don't Do This:** Mix multiple CSS preprocessors within the same project. Overuse complex preprocessor features, making the styles harder to understand.
**Example (using Sass):**
"""vue
"""
### 5.3 CSS Modules
**Standard:** Consider CSS Modules for component styling to avoid naming collisions and improve CSS encapsulation, especially in larger projects.
**Why:** CSS Modules automatically generate unique class names, preventing style conflicts between components.
**Do This:** Use the "
"""
### 5.4 Utility-First CSS (e.g., Tailwind CSS)
**Standard:** Consider using a utility-first CSS framework (e.g., Tailwind CSS) for rapid UI development and consistent styling.
**Why:** Utility-first frameworks provide a set of pre-defined utility classes that can be composed to create custom designs, reducing the need to write custom CSS.
**Do This:** Install and configure your chosen utility-first framework. Use utility classes directly in your component templates to style elements.
**Don't Do This:** Overuse custom CSS, defeating the purpose of the utility-first framework. Modify the framework's core styles unless absolutely necessary.
**Example (using Tailwind CSS):** This example assumes Tailwind CSS is already configured.
"""vue
"""
## 6. Component Testing
### 6.1 Unit Testing
**Standard:** Write unit tests for individual components to verify their behavior in isolation.
**Why:** Unit tests help catch bugs early, improve code quality, and facilitate refactoring.
**Do This:** Use a testing framework (e.g., Jest, Vitest) and a component testing library (e.g., Vue Test Utils, Testing Library). Test component inputs (props), outputs (emitted events), and rendered output.
**Don't Do This:** Skip unit tests or write tests that only verify implementation details.
**Example (using Vitest and Vue Test Utils):**
"""javascript
// MyComponent.spec.js
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import MyComponent from './MyComponent.vue';
describe('MyComponent', () => {
it('renders the message prop', () => {
const wrapper = mount(MyComponent, {
props: {
message: 'Hello, world!'
}
});
expect(wrapper.text()).toContain('Hello, world!');
});
it('emits an event when the button is clicked', async () => {
const wrapper = mount(MyComponent);
await wrapper.find('button').trigger('click');
expect(wrapper.emitted('custom-event')).toBeTruthy();
});
});
"""
### 6.2 End-to-End (E2E) Testing
**Standard:** Write E2E tests to verify the overall application functionality and user workflows.
**Why:** E2E tests ensure that all parts of the application work together correctly, simulating real user interactions.
**Do This:** Use an E2E testing framework (e.g., Cypress, Playwright). Write tests that cover critical user flows, such as login, form submission, and data display.
**Don't Do This:** Rely solely on E2E tests, as they can be slow and difficult to debug. E2E tests should complement unit tests, not replace them.
This document provides a comprehensive guide to component design standards in Vue.js. By following these guidelines, you can build high-quality, maintainable, and scalable Vue.js applications. Remember to adapt these standards to your specific project needs and team preferences.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# API Integration Standards for Vue.js This document outlines the coding standards for API integration in Vue.js applications. Adhering to these standards will improve the maintainability, performance, security, and overall quality of your Vue.js projects. These standards are informed by the latest Vue.js best practices and are designed to work seamlessly with modern tooling and workflows. ## 1. Architectural Patterns for API Integration ### 1.1. Centralized API Service Layer **Standard:** Implement a centralized API service layer comprised of dedicated modules or classes. These modules encapsulate the logic for making API requests, handling responses, and managing errors. Avoid direct API calls within components. **Why:** * **Maintainability:** Decouples components from API details, allowing for easier changes to API endpoints or data structures without affecting UI code. * **Testability:** Facilitates unit testing of API interaction logic independent of Vue components. * **Reusability:** Promotes reuse of API logic across multiple components. * **Centralized Error Handling**: Allows better error management and reduces code duplication. **Do This:** Create dedicated API service using composables or classes. **Don't Do This:** Embed API calls directly within Vue components. **Code Example (Composable approach):** """javascript // src/composables/useApi.js import { ref } from 'vue'; import axios from 'axios'; const useApi = (baseUrl) => { const data = ref(null); const loading = ref(false); const error = ref(null); const fetchData = async (endpoint, options = {}) => { loading.value = true; error.value = null; try { const response = await axios({ url: "${baseUrl}/${endpoint}", method: 'GET', // Default method ...options, }); data.value = response.data; return response.data } catch (err) { error.value = err; console.error('API Error:', err); // Consider a global error handling mechanism here throw err; // Re-throw to allow component-level handling. } finally { loading.value = false; } }; return { data, loading, error, fetchData }; }; export default useApi; // Example usage in a component // src/components/MyComponent.vue <template> <div v-if="loading">Loading...</div> <div v-if="error">Error: {{ error.message }}</div> <div v-if="posts"> <ul> <li v-for="post in posts" :key="post.id">{{ post.title }}</li> </ul> </div> </template> <script setup> import { ref, onMounted } from 'vue'; import useApi from '@/composables/useApi'; const { data: posts, loading, error, fetchData } = useApi('https://jsonplaceholder.typicode.com'); onMounted(() => { fetchData('/posts'); }); </script> """ **Code Example (Class-based approach):** """javascript // src/services/PostService.js import axios from 'axios'; class PostService { constructor(baseURL) { this.baseURL = baseURL; this.axiosInstance = axios.create({ baseURL: this.baseURL, }); } async getPosts() { try { const response = await this.axiosInstance.get('/posts'); return response.data; } catch (error) { console.error('Error fetching posts:', error); throw error; // Re-throw to let components handle errors. } } async getPost(id) { try { const response = await this.axiosInstance.get("/posts/${id}"); return response.data; } catch (error) { console.error("Error fetching post with ID ${id}:", error); throw error; } } // Add other methods for POST, PUT, DELETE, etc. } export default PostService; // usage in a component // src/components/MyComponent.vue <template> <div v-if="loading">Loading...</div> <div v-if="error">Error: {{ error.message }}</div> <div v-if="posts"> <ul> <li v-for="post in posts" :key="post.id">{{ post.title }}</li> </ul> </div> </template> <script setup> import { ref, onMounted } from 'vue'; import PostService from '@/services/PostService'; const postService = new PostService('https://jsonplaceholder.typicode.com'); const posts = ref([]); const loading = ref(true); const error = ref(null); onMounted(async () => { try { posts.value = await postService.getPosts(); } catch (err) { error.value = err; } finally { loading.value = false; } }); </script> """ ### 1.2. State Management Integration **Standard:** Integrate API data with Vuex (or Pinia) for global state management, providing a single source of truth for data fetched from the backend. **Why:** * **Data Consistency:** Ensures that data is consistent across multiple components. * **Simplified Data Sharing:** Allows components to easily access and modify API data without prop drilling or complex event handling. * **Centralized State Mutations:** Makes it easier to track changes to the application state and debug issues. * **Improved Performance:** Optimizes re-renders by leveraging Vuex's reactivity system. **Do This:** Utilize Vuex (or Pinia) to store and manage API data that is shared across multiple components. **Don't Do This:** Rely solely on component-local state for data that needs to be accessed or modified by other components. **Code Example (using Pinia):** """javascript // src/stores/posts.js import { defineStore } from 'pinia'; import axios from 'axios'; export const usePostsStore = defineStore('posts', { state: () => ({ posts: [], loading: false, error: null, }), getters: { getPostById: (state) => (id) => state.posts.find((post) => post.id === id), }, actions: { async fetchPosts() { this.loading = true; this.error = null; try { const response = await axios.get('https://jsonplaceholder.typicode.com/posts'); this.posts = response.data; } catch (err) { this.error = err; } finally { this.loading = false; } }, addPost(post) { this.posts.push(post) } }, }); // src/components/PostsList.vue <template> <div v-if="postsStore.loading">Loading...</div> <div v-if="postsStore.error">Error: {{ postsStore.error.message }}</div> <div v-if="postsStore.posts.length"> <ul> <li v-for="post in postsStore.posts" :key="post.id">{{ post.title }}</li> </ul> </div> </template> <script setup> import { onMounted } from 'vue'; import { usePostsStore } from '@/stores/posts'; const postsStore = usePostsStore(); onMounted(() => { if (postsStore.posts.length === 0) { postsStore.fetchPosts(); } }); </script> """ ### 1.3. Data Transformation Layer **Standard:** Implement a data transformation layer to adapt API responses to the specific needs of your Vue components. This could involve renaming fields, restructuring data, or calculating derived values. **Why:** * **Decoupling:** Shields the UI from changes in the API data structure. * **Data Consistency:** Ensures that the data used by components is always in the expected format. * **Performance:** Reduces the amount of data processing required in components. **Do This:** Create data transformation functions or classes to map API data to the format expected by your components. **Don't Do This:** Directly use raw API responses in your components without any transformation. **Code Example:** """javascript // src/utils/dataTransformations.js export const transformPostData = (apiPost) => { return { id: apiPost.id, title: apiPost.title, content: apiPost.body, // Renamed field authorName: 'Unknown', // Default value //... additional transformations }; }; // usage in a composable/service/component: import { transformPostData } from '@/utils/dataTransformations'; // Example within the useApi composable: const useApi = (baseUrl) => { // ... other code from previous example const fetchData = async (endpoint, options = {}) => { // ... const response = await axios({ url: "${baseUrl}/${endpoint}", method: 'GET', // Default method ...options, }); data.value = response.data.map(transformPostData); // Transformation applied here. return response.data } } """ ## 2. Implementation Best Practices ### 2.1. Using "async/await" **Standard:** Use "async/await" syntax for asynchronous API calls to improve code readability and reduce callback nesting. **Why:** * **Readability:** Makes asynchronous code easier to read and understand. * **Error Handling:** Simplifies error handling with "try/catch" blocks. * **Debugging:** Facilitates easier debugging of asynchronous code. **Do This:** Use "async/await" for making API calls and handling responses. **Don't Do This:** Rely on ".then()" and ".catch()" for asynchronous operations unless absolutely necessary. **Code Example:** """javascript // Correct using async/await. const fetchData = async () => { try { const response = await axios.get('/api/data'); this.data = response.data; } catch (error) { console.error('Error fetching data:', error); } }; // Anti-pattern: avoids then/catch where async/await is appropriate const fetchDataBad = () => { axios.get('/api/data') .then(response => { this.data = response.data; }) .catch(error => { console.error('Error fetching data:', error); }); }; """ ### 2.2. Error Handling **Standard:** Implement robust error handling for API calls, including displaying user-friendly error messages and logging errors for debugging. Use try/catch blocks in "async" functions or ".catch()" blocks for promises. Avoid generic error messages; provide context. **Why:** * **User Experience:** Provides informative feedback to users when API calls fail. * **Debugging:** Helps identify and fix issues in the application. * **Resilience:** Prevents the application from crashing due to API errors. * **Security**: Prevents revealing sensitive information in error messages. **Do This:** Wrap API calls in "try...catch" blocks, log errors, and display appropriate messages to the user. **Don't Do This:** Ignore errors or display generic error messages. **Code Example:** """javascript const fetchData = async () => { try { const response = await axios.get('/api/data'); this.data = response.data; } catch (error) { console.error('Error fetching data:', error); // Display a user-friendly error message. this.errorMessage = 'Failed to load data. Please try again later.'; if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx console.error("Data:", error.response.data); console.error("Status:", error.response.status); console.error("Headers:", error.response.headers); if (error.response.status === 404) { this.errorMessage = 'The requested resource was not found.'; } else if (error.response.status === 500) { this.errorMessage = 'A server error occurred. Please inform support.'; } } else if (error.request) { // The request was made but no response was received console.error("Request:", error.request); this.errorMessage = 'Could not connect to the server. Please check your internet connection.'; } else { // Something happened in setting up the request that triggered an Error console.error('Error', error.message); this.errorMessage = 'An unexpected error occurred.'; } } }; """ ### 2.3. Request Cancellation **Standard:** Implement request cancellation for long-running API calls, especially in scenarios where the user might navigate away from the page or trigger another request before the previous one completes. **Why:** * **Performance:** Prevents unnecessary resource consumption on the client and server. * **User Experience:** Avoids displaying stale or irrelevant data to the user. * **Data Integrity:** Prevents race conditions and potential data corruption. **Do This:** Use "axios.CancelToken" (or equivalent functionality in other HTTP clients) to cancel pending requests. **Don't Do This:** Allow long-running requests to continue executing in the background after they are no longer needed. **Code Example:** """javascript import axios from 'axios'; import { ref, onBeforeUnmount } from 'vue'; export default { setup() { const data = ref(null); const loading = ref(false); const error = ref(null); const cancelTokenSource = axios.CancelToken.source(); const fetchData = async () => { loading.value = true; error.value = null; try { const response = await axios.get('/api/data', { cancelToken: cancelTokenSource.token, }); data.value = response.data; } catch (err) { if (axios.isCancel(err)) { console.log('Request cancelled:', err.message); } else { error.value = err; } } finally { loading.value = false; } }; onBeforeUnmount(() => { cancelTokenSource.cancel('Component unmounted.'); // Cancel the request }); return { data, loading, error, fetchData }; }, }; """ ### 2.4. Data Caching **Standard:** Implement client-side caching for frequently accessed API data that does not change frequently. **Why:** * **Performance:** Reduces the number of API requests, improving application performance. * **User Experience:** Provides a faster and more responsive user interface. * **Offline Support:** Enables the application to function partially or fully offline. * **Cost Savings:** Reduces bandwidth consumption. **Do This:** Use browser storage (e.g., "localStorage", "sessionStorage", "IndexedDB"), Vuex persistence plugins, or dedicated caching libraries to store API data. **Don't Do This:** Cache sensitive data without proper encryption or store data indefinitely without invalidation. **Code Example (using localStorage):** """javascript import axios from 'axios'; const CACHE_KEY = 'api_data'; const CACHE_EXPIRY = 60 * 60 * 1000; // 1 hour const fetchData = async () => { const cachedData = localStorage.getItem(CACHE_KEY); const cachedTime = localStorage.getItem("${CACHE_KEY}_timestamp"); if (cachedData && cachedTime && (Date.now() - parseInt(cachedTime)) < CACHE_EXPIRY) { console.log('Using cached data'); return JSON.parse(cachedData); } try { const response = await axios.get('/api/data'); const data = response.data; localStorage.setItem(CACHE_KEY, JSON.stringify(data)); localStorage.setItem("${CACHE_KEY}_timestamp", Date.now().toString()); //store timestamp as string return data; } catch (error) { console.error('Error fetching data:', error); throw error; } }; """ ### 2.5. Request/Response Interceptors **Standard:** Utilize request and response interceptors provided by "axios" or similar libraries to add common headers, handle authentication, log requests, or transform responses globally. Use sparingly and carefully. **Why:** * **Code Reusability:** Avoids code duplication by centralizing common logic. * **Maintainability:** Simplifies global configuration changes. * **Security:** Enables automatic injection of authentication tokens. **Do This:** Use interceptors to implement cross-cutting concerns related to API requests and responses. **Don't Do This:** Overuse interceptors for component-specific logic or perform complex operations that impact performance. **Code Example:** """javascript // src/utils/axiosConfig.js import axios from 'axios'; // Request interceptor axios.interceptors.request.use( (config) => { // Add authentication token const token = localStorage.getItem('authToken'); if (token) { config.headers.Authorization = "Bearer ${token}"; } // Log the request console.log('Request:', config.method, config.url); return config; }, (error) => { console.error('Request Error:', error); return Promise.reject(error); } ); // Response interceptor axios.interceptors.response.use( (response) => { // Log the response console.log('Response:', response.status, response.data); return response; }, (error) => { console.error('Response Error:', error); // Handle unauthorized errors globally if (error.response && error.response.status === 401) { // Redirect to login page window.location.href = '/login'; } return Promise.reject(error); } ); export default axios; // Usage: Simply import the configured axios instance import axios from '@/utils/axiosConfig'; const fetchData = async () => { try { const response = await axios.get('/api/protected-data'); // ... } catch(error) { // ... } } """ ## 3. Security Considerations ### 3.1. Sensitive Data Handling **Standard:** Never store sensitive information (e.g., passwords, API keys) directly in the Vue.js codebase or client-side storage (localStorage, sessionStorage). Manage API keys securely using environment variables or a secure configuration management system. **Why:** * **Security:** Prevents unauthorized access to sensitive data. **Do This:** Use environment variables, backend services, or secure configuration management systems to store and manage sensitive data. **Don't Do This:** Store sensitive data directly in the codebase or client-side storage. ### 3.2. Cross-Origin Resource Sharing (CORS) **Standard:** Ensure that the backend API is properly configured with CORS to allow requests from the Vue.js application's origin. **Why:** * **Security:** Prevents unauthorized access to the API from other origins. **Do This:** Configure the backend API to allow requests from the Vue.js application's origin. **Don't Do This:** Disable CORS without understanding the security implications. The backend team should handle CORS configuration, not the frontend. ### 3.3. Input Validation and Sanitization **Standard:** Validate and sanitize all user inputs on both the client-side (Vue.js) and server-side to prevent injection attacks (e.g., XSS, SQL injection). **Why:** * **Security:** Prevents malicious code from being injected into the application. **Do This:** Use appropriate validation and sanitization techniques to protect against injection attacks. **Don't Do This:** Trust user input without validation or sanitization. ### 3.4. HTTPS **Standard:** Use HTTPS for all communication between the Vue.js application and the backend API. **Why:** * **Security:** Encrypts data in transit, preventing eavesdropping and man-in-the-middle attacks. **Do This:** Configure both the Vue.js application and the backend API to use HTTPS. **Don't Do This:** Use HTTP for sensitive data or production environments. ## 4. Tooling and Libraries ### 4.1. Axios **Standard:** Use "axios" as the primary HTTP client for making API requests in Vue.js applications unless there's a very good reason to use something else. It offers request cancellation, interceptors, and automatic JSON parsing. **Why:** * Widely used and well-supported. * Provides a consistent and intuitive API. * Supports request cancellation, interceptors, and automatic JSON parsing. **Do This:** Use axios for making API requests. **Don't Do This:** Re-invent the wheel by using the browser's "fetch" API directly for complex scenarios handled well by Axios. ### 4.2. Vue Devtools **Standard:** Use Vue Devtools browser extension for inspecting API requests and responses, component state, and Vuex (or Pinia) mutations. **Why:** * **Debugging:** Provides valuable insights into the application's behavior. * **Performance Optimization:** Helps identify performance bottlenecks. **Do This:** Install and use Vue Devtools for debugging and performance analysis. ## 5. GraphQL Considerations ### 5.1 Apollo Client **Standard:** Use Apollo Client, or similar, to integrate GraphQL APIs. **Why:** * Official Vue integration. * Caching, and state management. **Do This:** Use the Apollo Client to manage the client-side integration. ### 5.2 Query Design **Standard:** Design GraphQL queries to only fetch what is needed. Avoid over-fetching. **Why:** * To reduce payload size. * To improve performance. **Do This:** Fetch only the required data. ## 6. Testing API Integrations ### 6.1 Unit Testing **Standard:** Write unit tests for API service modules using tools like Jest or Vitest, mocking the actual HTTP requests to isolate the logic. **Why:** * Ensure that API interaction logic is working correctly. * Catch regressions early in the development process. **Do This:** Create comprehensive unit tests covering all API service methods. """javascript // Example unit test for PostService.js using Jest and mock-axios import PostService from '@/services/PostService'; import axios from 'axios'; jest.mock('axios'); // mock axios describe('PostService', () => { let postService; beforeEach(() => { postService = new PostService('https://example.com/api'); axios.get.mockClear(); // clear mock calls before each test }); it('fetches posts successfully', async () => { const mockPosts = [{ id: 1, title: 'Test Post' }]; axios.get.mockResolvedValue({ data: mockPosts }); const posts = await postService.getPosts(); expect(axios.get).toHaveBeenCalledWith('/posts'); expect(posts).toEqual(mockPosts); }); it('handles errors when fetching posts', async () => { const mockError = new Error('Failed to fetch posts'); axios.get.mockRejectedValue(mockError); await expect(postService.getPosts()).rejects.toThrow(mockError); expect(axios.get).toHaveBeenCalledWith('/posts'); }); }); """ ### 6.2 End-to-End (E2E) Testing **Standard:** Use end-to-end testing frameworks (e.g., Cypress, Playwright) to verify that the API integrations work correctly from the user's perspective, simulating real user interactions and validating the displayed data. **Why:** * Ensure that the entire application flow, including API interactions, is working as expected. * Identify integration issues that might not be caught by unit tests. **Do This:** Implement E2E tests that cover the critical API-driven features of the application. These standards provide a comprehensive guide for API integration in Vue.js applications. Adhering to these guidelines will result in more maintainable, performant, and secure applications. Remember, these are guidelines, and thoughtful deviations can be made when justified by specific project requirements.
# Code Style and Conventions Standards for Vue.js This document outlines the coding style and conventions standards for Vue.js projects. Adhering to these standards ensures code readability, maintainability, and consistency across all projects. This guide is designed to be used by developers and as context for AI coding assistants. It’s aligned with the latest Vue.js features and best practices. ## 1. General Formatting ### 1.1. Whitespace and Indentation **Standard:** Use 2 spaces for indentation. Avoid tabs to ensure consistency across different editors and systems. **Do This:** """vue <template> <div> <p>Hello, Vue!</p> </div> </template> <script setup> const message = 'Hello, Vue!'; </script> """ **Don't Do This:** """vue <template> <div> <p>Hello, Vue!</p> </div> </template> <script setup> const message = 'Hello, Vue!'; </script> """ **Why:** Consistent indentation significantly improves code readability. Using spaces instead of tabs avoids rendering issues across different IDE configurations. ### 1.2. Line Length **Standard:** Limit line length to 120 characters. This enhances readability, especially on smaller screens or split-screen setups. **Do This:** """javascript const myVeryLongVariableName = 'This is a long string that should be broken into multiple lines to improve readability.'; const result = someFunction( myVeryLongVariableName, anotherLongVariableName, yetAnotherLongVariableName ); """ **Don't Do This:** """javascript const myVeryLongVariableName = 'This is a very long string that really hurts readability when it all exists on a single line!'; """ **Why:** Shorter lines improve readability, reducing horizontal scrolling and making it easier to compare different versions of the code. ### 1.3. File Structure and Organization **Standard:** Organize Vue components into separate files. Use a clear directory structure based on features, modules, or routes based on project scale and complexity. For larger projects, consider using a modular approach with separate directories for components, composables, and views. **Do This:** """ src/ ├── components/ │ ├── MyComponent.vue │ └── AnotherComponent.vue ├── composables/ │ ├── useMyComposable.js │ └── useAnotherComposable.js ├── views/ │ ├── HomeView.vue │ └── AboutView.vue └── App.vue """ **Don't Do This:** """ src/ ├── MyComponent.vue ├── AnotherComponent.vue ├── useMyComposable.js ├── HomeView.vue ├── AboutView.vue └── App.vue """ **Why:** Segregating concerns into separate files makes it easier to find, understand, and maintain code. ### 1.4. Component File Structure **Standard:** Organize Vue Single File Components (SFCs) in a standard order: "<template>", "<script>", "<style>". For "<script>", prefer using "<script setup>" syntax. **Do This:** """vue <template> <div> <h1>{{ message }}</h1> </div> </template> <script setup> import { ref } from 'vue'; const message = ref('Hello, Vue!'); </script> <style scoped> h1 { color: blue; } </style> """ **Don't Do This:** """vue <script> export default { data() { return { message: 'Hello, Vue!' } } } </script> <template> <div> <h1>{{ message }}</h1> </div> </template> <style scoped> h1 { color: blue; } </style> """ **Why:** A consistent structure makes it easy to navigate and understand components, making the development process faster and more predictable. "<script setup>" provides a concise and performant syntax. ## 2. Naming Conventions ### 2.1. Component Names **Standard:** Use PascalCase for component names. This applies to both file names and component names when registering them. Use descriptive names that clearly indicate the component’s purpose. **Do This:** """vue // MyButton.vue export default { name: 'MyButton' // ... } """ """vue <template> <MyButton /> </template> <script setup> import MyButton from './components/MyButton.vue'; </script> """ **Don't Do This:** """vue // my-button.vue (Incorrect filename) export default { name: 'myButton' // Incorrect component name // ... } """ **Why:** PascalCase distinguishes components from HTML elements and improves readability. Descriptive names convey the component's purpose. ### 2.2. Prop Names **Standard:** Use camelCase for prop names in JavaScript and kebab-case in the template. Define prop types explicitly. **Do This:** """vue // MyComponent.vue <template> <div> <h1>{{ myMessage }}</h1> </div> </template> <script setup> defineProps({ myMessage: { type: String, required: true } }); </script> """ """vue <template> <MyComponent my-message="Hello" /> </template> """ **Don't Do This:** """vue // MyComponent.vue <template> <div> <h1>{{ my_message }}</h1> </div> </template> <script> export default { props: ['my_message'] } </script> <template> <MyComponent myMessage = 'Hello'></MyComponent> </template> """ **Why:** camelCase is the standard JavaScript naming convention, while kebab-case is the standard for HTML attributes. Explicit prop type definitions enhance code reliability and maintainability. ### 2.3. Event Names **Standard:** Use kebab-case for event names emitted by components. This aligns with HTML attribute conventions. Prefix custom events with the component name to avoid naming conflicts. **Do This:** """vue // MyComponent.vue <script setup> import { defineEmits } from 'vue'; const emit = defineEmits(['my-component:item-selected']); const selectItem = (item) => { emit('my-component:item-selected', item); }; </script> """ """vue <template> <MyComponent @my-component:item-selected="handleItemSelected" /> </template> """ **Don't Do This:** """vue // MyComponent.vue <script> export default { methods: { selectItem(item) { this.$emit('itemSelected', item); } } } </script> <template> <MyComponent @itemSelected="handleItemSelected" /> </template> """ **Why:** kebab-case is the standard for HTML attributes (and therefore event listeners). Prefixing mitigates potential naming collisions when using multiple components. ### 2.4. Variable Names **Standard:** Use camelCase for variable names in JavaScript. Use descriptive and meaningful names. **Do This:** """javascript const numberOfItems = 10; const selectedItem = null; """ **Don't Do This:** """javascript const num = 10; const sel = null; """ **Why:** Descriptive variable names make code easier to understand and maintain. ### 2.5. Constant Names **Standard:** Use UPPER_SNAKE_CASE for constants. **Do This:** """javascript const MAX_ITEMS = 20; const API_URL = 'https://example.com/api'; """ **Don't Do This:** """javascript const maxItems = 20; const apiUrl = 'https://example.com/api'; """ **Why:** UPPER_SNAKE_CASE clearly identifies constants, improving code readability. ## 3. Stylistic Consistency ### 3.1. Quotes **Standard:** Use single quotes (') for strings unless you need to embed variables. **Do This:** """javascript const message = 'Hello, Vue!'; const greeting = "Hello, ${name}!"; """ **Don't Do This:** """javascript const message = "Hello, Vue!"; """ **Why:** Consistency in quote usage reduces cognitive load and makes code cleaner. ### 3.2. Curly Braces **Standard:** Always use curly braces for "if", "else", "for", and "while" statements, even if the block contains only one statement. **Do This:** """javascript if (isTrue) { console.log('It is true.'); } else { console.log('It is false.'); } """ **Don't Do This:** """javascript if (isTrue) console.log('It is true.'); else console.log('It is false.'); """ **Why:** Using curly braces consistently reduces ambiguity and potential errors. ### 3.3. Semicolons **Standard:** Use semicolons at the end of each statement. While JavaScript allows omitting semicolons in some cases, explicitly including them avoids potential pitfalls due to automatic semicolon insertion (ASI). **Do This:** """javascript const message = 'Hello, Vue!'; console.log(message); """ **Don't Do This:** """javascript const message = 'Hello, Vue!' console.log(message) """ **Why:** Explicit semicolons prevent unexpected behavior caused by ASI. ### 3.4. Template Syntax **Standard:** Use consistent template syntax. Prefer v-bind shorthand (":") and v-on shorthand ("@"). **Do This:** """vue <template> <button :disabled="isDisabled" @click="handleClick">Click me</button> </template> """ **Don't Do This:** """vue <template> <button v-bind:disabled="isDisabled" v-on:click="handleClick">Click me</button> </template> """ **Why:** Shorthand syntax is more concise and readable. ### 3.5. Component Props **Standard:** When passing multiple props to a component, use multiple lines for better readability, especially when the values are complex expressions. **Do This:** """vue <template> <MyComponent :prop1="value1" :prop2="value2" :prop3="value3" /> </template> """ **Don't Do This:** """vue <template> <MyComponent :prop1="value1" :prop2="value2" :prop3="value3" /> </template> """ **Why:** Multiline syntax enhances readability, especially when prop values are long or complex expressions. ## 4. Vue.js Specific Conventions ### 4.1. Using "<script setup>" **Standard:** Prefer "<script setup>" syntax for Vue 3 components. It offers better performance, more concise syntax, and improved type inference compared to the options API. **Do This:** """vue <template> <div> <h1>{{ message }}</h1> </div> </template> <script setup> import { ref } from 'vue'; const message = ref('Hello, Vue!'); </script> """ **Don't Do This:** """vue <template> <div> <h1>{{ message }}</h1> </div> </template> <script> export default { data() { return { message: 'Hello, Vue!' }; } }; </script> """ **Why:** "<script setup>" is more performant and reduces boilerplate code compared to the Options API. It handles component registration automatically and provides better type inference, which reduces the need for explicit type annotations. ### 4.2. Reactive Properties with "ref" and "reactive" **Standard:** Use "ref" for primitive values and "reactive" for complex objects. **Do This:** """javascript import { ref, reactive } from 'vue'; const count = ref(0); const user = reactive({ name: 'John Doe', age: 30 }); """ **Don't Do This:** """javascript import { reactive } from 'vue'; const count = reactive(0); // Incorrect, should use ref for primitive values. """ **Why:** "ref" creates a single reactive reference, suitable for primitive values. "reactive" makes all properties of an object reactive, which is more efficient for complex objects. ### 4.3. Computed Properties **Standard:** Use computed properties for derived data. Avoid performing complex logic directly in the template. **Do This:** """vue <template> <div> <p>Full Name: {{ fullName }}</p> </div> </template> <script setup> import { ref, computed } from 'vue'; const firstName = ref('John'); const lastName = ref('Doe'); const fullName = computed(() => "${firstName.value} ${lastName.value}"); </script> """ **Don't Do This:** """vue <template> <div> <p>Full Name: {{ firstName }} {{ lastName }}</p> <!-- Mixing reactive data directly --> </div> </template> <script setup> import { ref } from 'vue'; const firstName = ref('John'); const lastName = ref('Doe'); </script> """ **Why:** Computed properties cache their results and only re-evaluate when their dependencies change, which enhances performance. They also keep the template cleaner and easier to read. ### 4.4. Watchers **Standard:** Use watchers sparingly. Prefer computed properties when possible. Use watchers primarily for side effects, such as updating local storage or making API calls based on data changes. For simple reactions to a single reactive property, consider "watchEffect". **Do This:** """javascript import { ref, watch } from 'vue'; const count = ref(0); watch(count, (newValue, oldValue) => { console.log("Count changed from ${oldValue} to ${newValue}"); // Perform a side effect, e.g., saving to localStorage localStorage.setItem('count', newValue); }); """ **Don't Do This:** """javascript import { ref, watch } from 'vue'; const count = ref(0); const doubledCount = ref(0); watch(count, (newValue) => { doubledCount.value = newValue * 2; // Use a computed property instead! }); """ **Why:** Watchers execute arbitrary code in response to changes, which can make the application’s state harder to reason about. Computed properties are generally preferable for deriving data. ### 4.5. Emitting Events **Standard:** Always declare emitted events using "defineEmits". This improves type safety and documentation. **Do This:** """vue <script setup> const emit = defineEmits(['item-selected', 'delete']); const selectItem = (item) => { emit('item-selected', item); }; </script> """ **Don't Do This:** """vue <script setup> const selectItem = (item) => { emit('item-selected', item); // Implicitly emitting events }; </script> """ **Why:** Declaring emitted events provides type safety and enables Vue to validate event listeners, which helps catch errors early in development. ### 4.6. Component Communication **Standard:** Prefer props and events for parent-child component communication. For more complex use cases, consider using a dedicated state management library such as Pinia, or Vue's built-in "provide"/"inject" mechanism. **Do This:** """vue // ParentComponent.vue <template> <ChildComponent :message="parentMessage" @child-event="handleChildEvent" /> </template> <script setup> import { ref } from 'vue'; import ChildComponent from './ChildComponent.vue'; const parentMessage = ref('Hello from parent!'); const handleChildEvent = (data) => { console.log('Received from child:', data); }; </script> """ """vue // ChildComponent.vue <template> <button @click="emitEvent">Send Message</button> </template> <script setup> import { defineProps, defineEmits } from 'vue'; defineProps({ message: { type: String, required: true } }); const emit = defineEmits(['child-event']); const emitEvent = () => { emit('child-event', 'Hello from child!'); }; </script> """ **Don't Do This:** """vue // Avoid directly mutating props from the child component // ParentComponent.vue <template> <ChildComponent :message="parentMessage" /> </template> <script setup> import { ref } from 'vue'; import ChildComponent from './ChildComponent.vue'; const parentMessage = ref('Hello from parent!'); </script> // ChildComponent.vue <template> <div>{{ message }}</div> </template> <script setup> import { defineProps, onMounted } from 'vue'; const props = defineProps({ message: { type: String, required: true } }); onMounted(() => { props.message = "Trying to change this"; // WRONG! Mutating props is an anti-pattern. }) </script> """ **Why:** Props and events create a clear and predictable data flow. Directly mutating parent-provided props can lead to unpredictable bugs and should be avoided. ### 4.7. Component Directives **Standard:** Write custom directives using the directive lifecycle hooks ("created", "mounted", "updated", "unmounted"). Ensure proper cleanup in the "unmounted" hook to prevent memory leaks. **Do This:** """javascript // v-focus.js export default { mounted: (el) => { el.focus(); }, unmounted: (el) => { // Cleanup if needed } }; // In Component: import focusDirective from ‘./v-focus.js'; <input v-focus /> """ **Don't Do This:** """javascript // Do not create directives with side-effects that cannot be cleaned up. export default { mounted: (el) => { // No unmounted hook - memory leak likely setInterval(() => {console.log("doing something")}, 1000) } }; """ ### 4.8. Async/Await **Standard:** Use "async/await" consistently for asynchronous operations to improve code readability and maintainability. Handle errors with "try/catch" blocks. **Do This:** """javascript const fetchData = async () => { try { const response = await fetch('https://example.com/api/data'); const data = await response.json(); return data; } catch (error) { console.error('Error fetching data:', error); return null; } }; """ **Don't Do This:** """javascript const fetchData = () => { fetch('https://example.com/api/data') .then(response => response.json()) .then(data => { // ... }) .catch(error => { console.error('Error fetching data:', error); }); }; """ **Why:** "async/await" makes asynchronous code look and behave more like synchronous code, which improves readability and makes error handling easier and more consistent. Using "try/catch" ensures proper error handling for "async/await". ### 4.9. Lazy Loading Components **Standard:** Use lazy-loading for non-critical components, especially those below the fold or in less-used sections of the application. Use "Suspense" to provide a fallback while the component is loading. **Do This:** """vue <template> <div> <Suspense> <template #default> <MyComponent /> </template> <template #fallback> <div>Loading...</div> </template> </Suspense> </div> </template> <script setup> import { defineAsyncComponent } from 'vue'; const MyComponent = defineAsyncComponent(() => import('./MyComponent.vue')); </script> """ **Don't Do This:** Don't load all components eagerly. **Why:** Lazy loading improves initial page load performance by deferring the loading of non-critical components until they are needed. ## 5. Anti-Patterns ### 5.1. Mutating Props **Anti-Pattern:** Avoid directly mutating props passed to a component. Props should be treated as read-only data. If you need to modify the value of a prop, emit an event to the parent component and let the parent handle the modification. **Why:** Mutating props violates the one-way data flow principle, making it difficult to reason about the application's state and leading to unpredictable behavior. ### 5.2. Global State Mutation **Anti-Pattern:** Avoid directly mutating global state or Vuex store state outside of mutations (or actions in Vuex). This makes it difficult to track where state changes originate and can lead to unexpected side effects. Use Pinia for state management in new projects for a more streamlined experience. **Why:** Mutations provide a centralized and trackable way to modify state, making it easier to debug and maintain the application. Using Pinia simplifies state management and provides a more intuitive API. ### 5.3. Overusing "forceUpdate" **Anti-Pattern:** Avoid using "this.$forceUpdate()" unless absolutely necessary. This forces a re-render of the component and its children, which can be inefficient. Instead, rely on Vue's reactivity system to automatically update the component when its data changes. **Why:** "forceUpdate" bypasses Vue's reactivity system, which can lead to performance issues and unexpected behavior. Ensure that your data is reactive and that Vue is tracking dependencies correctly. ### 5.4. Excessive DOM Manipulation **Anti-Pattern:** Avoid directly manipulating the DOM using JavaScript as much as possible. Vue's virtual DOM and templating system are designed to efficiently update the DOM based on data changes. **Why:** Direct DOM manipulation can interfere with Vue's rendering process and lead to performance issues. Rely on Vue's templating system and directives to manage DOM updates. ### 5.5. Inefficient Loops **Anti-Pattern:** Avoid performing complex computations or API calls within "v-for" loops. This can lead to performance issues, especially when rendering large lists. **Why:** Complex operations inside loops can significantly slow down rendering. Pre-compute the necessary data or use techniques like pagination or virtualization to optimize performance. ## 6. Performance Optimization ### 6.1. Key Attribute in "v-for" **Standard:** Always use the "key" attribute when rendering lists with "v-for". The "key" attribute should be a unique identifier for each item in the list. **Do This:** """vue <template> <ul> <li v-for="item in items" :key="item.id">{{ item.name }}</li> </ul> </template> """ **Why:** The "key" attribute helps Vue efficiently update the DOM when the list changes (e.g., items are added, removed, or reordered). Without the "key" attribute, Vue may re-render the entire list, leading to performance issues. Using index as a key is an anti-pattern unless the dataset will never change. ### 6.2. Component Caching **Standard:** Use "keep-alive" to cache frequently used components, especially those that are expensive to render. **Do This:** """vue <template> <keep-alive> <component :is="activeComponent" /> </keep-alive> </template> """ **Why:** "keep-alive" caches the component instance, preserving its state and reducing the need for re-rendering when the component is activated again. ### 6.3. Debouncing and Throttling **Standard:** Use debouncing and throttling to limit the rate at which event handlers are executed, especially for events like "input", "scroll", and "resize". **Why:** Debouncing and throttling prevent event handlers from being executed excessively, which can improve performance and reduce unnecessary computations or API calls. ### 6.4. Virtualized Lists **Standard:** Use virtualized lists for rendering large datasets. Virtualized lists only render the items that are currently visible, which significantly improves performance. **Why:** Virtualized lists reduce the amount of DOM that needs to be rendered, which improves scrolling performance and reduces memory usage. ### 6.5. Code Splitting **Standard:** Implement code splitting to break your application into smaller chunks that can be loaded on demand. This reduces the initial load time and improves performance. **Why:** Code splitting allows the browser to download only the code that is needed for the current page or feature, which improves the initial load time and reduces bandwidth usage. ## 7. Security Best Practices ### 7.1. Sanitizing User Input **Standard:** Always sanitize user input to prevent cross-site scripting (XSS) attacks. Use Vue's templating system, which automatically escapes HTML entities, or a dedicated sanitization library like DOMPurify. **Why:** Sanitizing user input prevents malicious code from being injected into your application, protecting users from XSS attacks. ### 7.2. Avoiding "eval" **Standard:** Avoid using the "eval" function, which can execute arbitrary JavaScript code. **Why:** "eval" can introduce security vulnerabilities if it's used to execute untrusted code. It also makes your code harder to debug and optimize. ### 7.3. Server-Side Rendering (SSR) Considerations **Standard:** When using server-side rendering (SSR), be aware of potential security risks such as XSS and cross-site request forgery (CSRF). Implement appropriate security measures on the server side, such as input validation, output encoding, and CSRF protection. **Why:** SSR introduces additional security considerations because the server is responsible for rendering the initial HTML. Failing to implement proper security measures can expose your application to attacks.
# Testing Methodologies Standards for Vue.js This document outlines the testing methodologies standards for Vue.js applications. It provides guidelines for unit, integration, and end-to-end testing, with a focus on best practices for maintainability, performance, and security. All examples will reflect modern Vue.js syntax and patterns with a focus on Vue 3 and its ecosystem. ## 1. General Testing Principles ### 1.1 Write Testable Code **Do This:** Design components with testability in mind. This means keeping components small, focused, and loosely coupled. Use dependency injection where appropriate to mock dependencies during testing. **Don't Do This:** Create large, monolithic components that are difficult to isolate and test. Avoid tight coupling between components, as this makes it harder to mock dependencies. **Why:** Testable code is easier to understand, maintain, and refactor. It also leads to more comprehensive and reliable tests. **Example:** """vue // Good: Testable component with dependency injection <template> <div>{{ message }}</div> </template> <script> import { inject, ref, onMounted } from 'vue'; export default { setup() { const apiService = inject('apiService'); const message = ref(''); onMounted(async () => { message.value = await apiService.fetchMessage(); }); return { message }; } }; </script> // Bad: Hard to test component with tight coupling <template> <div>{{ message }}</div> </template> <script> import { ref, onMounted } from 'vue'; import apiService from './apiService'; // Tight coupling export default { setup() { const message = ref(''); onMounted(async () => { message.value = await apiService.fetchMessage(); }); return { message }; } }; </script> """ ### 1.2 Test-Driven Development (TDD) **Do This:** Write tests before writing the code. Write a failing test first, then implement the functionality to make the test pass. **Don't Do This:** Write tests after the code is written. This often leads to tests that are less comprehensive and may miss edge cases. **Why:** TDD helps to ensure that code meets requirements and encourages design that is testable from the outset. ### 1.3 Aim for High Test Coverage **Do This:** Strive for a high level of test coverage (e.g., 80% or higher). Use code coverage tools to identify areas of code that are not covered by tests. **Don't Do This:** Assume that code is well-tested simply because you have some tests in place. Neglect code coverage analysis. **Why:** High test coverage reduces the risk of bugs and improves the overall quality of the software. ### 1.4 Write Clear and Concise Tests **Do This:** Write tests in a clear, concise, and readable manner. Use descriptive names for tests and assertions. Avoid complex logic within tests. **Don't Do This:** Write tests that are difficult to understand or maintain. Use vague or ambiguous test names. **Why:** Clear tests are easier to maintain and debug. They also serve as documentation for the code they are testing. ### 1.5 Automate Tests **Do This:** Integrate tests into the development workflow using tools like CI/CD pipelines which automatically runs test suites. **Don't Do This:** Manually run tests. This is time-consuming and error-prone. **Why:** Automated tests ensure that tests are run consistently and frequently, reducing the risk of regressions. ## 2. Unit Testing ### 2.1 Component Isolation **Do This:** Unit test components in isolation, mocking out dependencies, the "$emit" function and external services. This is typically done with libraries like Jest and Vue Testing Library. **Don't Do This:** Unit test components in combination when a unit test suffices. This often leads to brittle tests that are more prone to failure. **Why:** Isolation helps to focus tests on specific components, making them more reliable and easier to maintain. **Example:** """javascript // Unit test for a component using Vue Testing Library and Jest import { render, screen, fireEvent } from '@testing-library/vue'; import MyButton from './MyButton.vue'; describe('MyButton', () => { it('renders the button with the correct text', () => { render(MyButton, { props: { label: 'Click me' } }); expect(screen.getByText('Click me')).toBeInTheDocument(); }); it('emits an event when clicked', async () => { const { emitted } = render(MyButton, { props: { label: 'Click me' } }); const button = screen.getByText('Click me'); await fireEvent.click(button); expect(emitted().click).toBeTruthy(); }); }); """ ### 2.2 Testing Props and Events **Do This:** Verify that components render correctly based on the props they receive. Ensure that events which emit are done so with appropriate payloads. **Don't Do This:** Neglect testing how different prop values affect rendering or if events are missing required data. **Why:** Props and events are the primary way components communicate. Accurate testing of these interfaces is crucial. **Example:** """javascript // Testing props and emitted events import { render, screen, fireEvent } from '@testing-library/vue'; import MyComponent from './MyComponent.vue'; describe('MyComponent', () => { it('renders with the correct title based on the title prop', () => { render(MyComponent, { props: { title: 'Hello World' } }); expect(screen.getByText('Hello World')).toBeInTheDocument(); }); it('emits a "custom-event" with the correct payload when clicked', async () => { const { emitted } = render(MyComponent); const button = screen.getByRole('button'); await fireEvent.click(button); expect(emitted()['custom-event'][0][0]).toEqual({ message: 'Button clicked' }); // Verifies the event and payload }); }); """ ### 2.3 Testing Computed Properties **Do This:** Test computed properties to make sure they computed correctly on component data. **Don't Do This:** Directly test the component data if the outcome is through a computed property. **Why:** Computed properties are central to reactivity and ensuring they function correctly is important for reliable state management. **Example:** """javascript // Testing a component with a computed property import { render, screen } from '@testing-library/vue'; import MyComponent from './MyComponent.vue'; describe('MyComponent', () => { it('displays the full name correctly when first and last name props are provided', () => { render(MyComponent, { props: { firstName: 'John', lastName: 'Doe', }, }); expect(screen.getByText('Full Name: John Doe')).toBeInTheDocument(); }); it('displays a default message when either first or last name is missing', () => { render(MyComponent, { props: { firstName: 'John', lastName: '', }, }); expect(screen.getByText('Full Name: N/A')).toBeInTheDocument(); }); }); """ ### 2.4 Mocking External Dependencies **Do This:** Use mocking libraries (e.g., Jest mocks) to isolate the component and control the behavior of external dependencies like API requests or external libraries. The "vi" library is rising in popularity. **Don't Do This:** Rely on real API endpoints or external services during unit testing. **Why:** Mocking external dependencies allows you to test the component's logic without being affected by external factors. **Example:** """javascript // Mocking an API call using Jest mocks import { render, screen, waitFor } from '@testing-library/vue'; import MyComponent from './MyComponent.vue'; import apiService from './apiService'; jest.mock('./apiService'); describe('MyComponent', () => { it('fetches and displays data correctly', async () => { // Mock the API response apiService.fetchData.mockResolvedValue({ id: 1, name: 'Test Data' }); render(MyComponent); // Wait for the data to load and be displayed await waitFor(() => { expect(screen.getByText('Name: Test Data')).toBeInTheDocument(); }); expect(apiService.fetchData).toHaveBeenCalledTimes(1); }); it('handles errors correctly', async () => { // Mock the API to reject with an error apiService.fetchData.mockRejectedValue(new Error('API Error')); render(MyComponent); // Wait for the error message to be displayed await waitFor(() => { expect(screen.getByText('Error: API Error')).toBeInTheDocument(); }); }); }); """ ### 2.5 Testing Vuex/Pinia Actions and Getters (If Applicable) **Do This:** Unit test Vuex/Pinia actions and getters in isolation to ensure they perform as expected. **Don't Do This:** Neglect to test Vuex/Pinia actions and getters. **Why:** Vuex/Pinia actions and getters are responsible for managing the application state. Testing them ensures the accuracy of state management is maintained. """javascript // Pinia action and getter testing example using Jest import { createPinia, setActivePinia } from 'pinia'; import { useMyStore } from './store'; // Assuming store.js exports useMyStore describe('MyStore', () => { beforeEach(() => { // creates a fresh pinia and make it active so it's automatically picked // up by any useStore() call without having to pass it to it: // "useStore(pinia)" setActivePinia(createPinia()); }); it('should update the count when increment is called', () => { const store = useMyStore(); store.increment(); expect(store.count).toBe(1); }); it('should decrement the count when decrement is called', () => { const store = useMyStore(); store.decrement(); expect(store.count).toBe(-1); }); it('should return the correct evenOrOdd value', () => { const store = useMyStore(); // Initial count value is 0 expect(store.evenOrOdd).toBe('even'); store.increment(); // Count becomes 1 expect(store.evenOrOdd).toBe('odd'); }); }); """ ## 3. Integration Testing ### 3.1 Component Interactions **Do This:** Integration test how components interact with each other in a realistic scenario by rendering multiple components together. **Don't Do This:** Assume components work together correctly based solely on unit tests. Avoid testing how components respond to different user interactions. **Why:** Component interactions reveal bugs that may not be apparent during unit testing. **Example:** """javascript // Integration testing of two components import { render, screen, fireEvent } from '@testing-library/vue'; import ParentComponent from './ParentComponent.vue'; import ChildComponent from './ChildComponent.vue'; describe('ParentComponent', () => { it('updates the message in ParentComponent when button in ChildComponent is clicked', async () => { render(ParentComponent); const button = screen.getByText('Click me'); // Confirm the initial value of messageInParent expect(screen.getByText('Message from parent: Initial Message')).toBeInTheDocument(); await fireEvent.click(button); // Confirm that the message in ParentComponent has been updated expect(screen.getByText('Message from parent: Message from child')).toBeInTheDocument(); }); }); """ ### 3.2 Vue Router Navigation **Do This:** Test navigation guards and how routes transitions work in your application using "vue-router". Mock "$router" calls or use navigation APIs directly. **Don't Do This:** Neglect to ensure route parameters are passed correctly and components are rendered post-navigation. **Why:** Navigation is crucial in most SPAs for usability. This ensures core user flows are not broken by navigation issues. **Example:** """javascript // Integration testing with Vue Router import { createMemoryHistory, createRouter } from 'vue-router'; import { render, screen, fireEvent, waitFor } from '@testing-library/vue'; import MyComponent from './MyComponent.vue'; const routes = [ { path: '/', component: { template: '<div>Home</div>' } }, { path: '/about', component: { template: '<div>About</div>' } }, ]; const router = createRouter({ history: createMemoryHistory(), routes, }); describe('MyComponent with Router', () => { it('navigates to the "/about" route when the about link is clicked', async () => { router.push('/'); // Start at the home route render(MyComponent, { global: { plugins: [router], }, }); // Get the router link for /about const aboutLink = screen.getByRole('link', { name: 'About' }); // Fire the click event await fireEvent.click(aboutLink); // Wait for the navigation to complete before checking the DOM await waitFor(() => { expect(router.currentRoute.value.path).toBe('/about'); expect(screen.getByText('About')).toBeInTheDocument(); }); }); }); """ ### 3.3 API Calls **Do This:** Test if components correctly fetch and process data from external APIs using tools like "axios-mock-adapter". **Don't Do This:** Call real APIs during testing. This can slow down the testing process and may lead to inconsistent results. **Why:** Correct data handling is critical for application functionality. Validating API use enhances reliability. **Example:** """javascript // Integration testing with a mocked Axios API import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { render, screen, waitFor } from '@testing-library/vue'; import MyComponent from './MyComponent.vue'; const mock = new MockAdapter(axios); describe('MyComponent with API', () => { afterEach(() => { mock.reset(); // Reset the mock adapter after each test }); it('fetches and displays data correctly from a mock API', async () => { // Define the API URL and the mock response data const apiUrl = '/api/data'; const mockData = { id: 1, name: 'Mocked Data' }; // Set up the mock to intercept the API call and return mock data mock.onGet(apiUrl).reply(200, mockData); render(MyComponent); // Wait for the data to load and be displayed await waitFor(() => { expect(screen.getByText('Name: Mocked Data')).toBeInTheDocument(); }); }); it('handles errors gracefully when the API call fails', async () => { const apiUrl = '/api/data'; mock.onGet(apiUrl).reply(500, { message: 'Server Error' }); render(MyComponent); await waitFor(() => { expect(screen.getByText('Error: Request failed with status code 500')).toBeInTheDocument(); }); }); }); """ ### 3.4 Vuex/Pinia Store Interactions (If Applicable) **Do This:** Test how components interact with the Vuex/Pinia store including interactions with state, mutations and actions. **Don't Do This:** Neglect testing how components respond to state changes or how actions are dispatched in reaction to user actions. **Why:** Store validation ensures consistent and correct behaviour around store dispatches. **Example:** """javascript // Testing component interactions with Pinia store import { createPinia, setActivePinia } from 'pinia'; import { render, screen, fireEvent } from '@testing-library/vue'; import MyComponent from './MyComponent.vue'; import { useMyStore } from './store'; // Assuming store.js exports useMyStore describe('MyComponent with Pinia Store', () => { beforeEach(() => { // creates a fresh pinia and make it active so it's automatically picked // up by any useStore() call without having to pass it to it: // "useStore(pinia)" setActivePinia(createPinia()); }); it('displays the count from the store and dispatches an action when clicked', async () => { render(MyComponent); const store = useMyStore(); // Initial Count expect(screen.getByText('Count: 0')).toBeInTheDocument(); // Increment count when Increment Button is clicked const incrementButton = screen.getByText('Increment'); await fireEvent.click(incrementButton); expect(screen.getByText('Count: 1')).toBeInTheDocument(); // Decrement count when Decrement Button is clicked const decrementButton = screen.getByText('Decrement'); await fireEvent.click(decrementButton); expect(screen.getByText('Count: 0')).toBeInTheDocument(); // Ensure action was dispatched the right number of times expect(store.increment).toHaveBeenCalledTimes(1); expect(store.decrement).toHaveBeenCalledTimes(1); }); }); """ ## 4. End-to-End (E2E) Testing ### 4.1 Realistic User Flows **Do This:** Emulate real user flows using tools like Cypress or Playwright. Login, navigate, interact with forms, and more. **Don't Do This:** Write E2E tests that are too narrow or do not mirror real user interactions. Avoid testing basic UI elements without a complete scenario. **Why:** E2E confirms the application works as expected from a user's perspective, covering multiple layers of the application. **Example (Cypress):** """javascript // End-to-end test using Cypress describe('User Login Flow', () => { it('allows a user to log in and view the dashboard', () => { cy.visit('/login'); cy.get('input[name=email]').type('test@example.com'); cy.get('input[name=password]').type('password123'); cy.get('button[type=submit]').click(); cy.url().should('include', '/dashboard'); cy.contains('Welcome to the Dashboard').should('be.visible'); }); it('displays an error message for invalid login credentials', () => { cy.visit('/login'); cy.get('input[name=email]').type('invalid@example.com'); cy.get('input[name=password]').type('wrongpassword'); cy.get('button[type=submit]').click(); cy.contains('Invalid credentials').should('be.visible'); }); }); """ ### 4.2 Cross-Browser Testing **Do This:** Run E2E tests across multiple browsers (Chrome, Firefox, Safari, Edge) and ensure consistent application behaviour. **Don't Do This:** Test only in the primary development browser. **Why:** Cross-browser testing uncovers rendering and compatibility issues that may exist only on specific browsers. ### 4.3 Testing API Integrations **Do This:** Verify UI components correctly interact with backend services and handle real-world data scenarios. **Don't Do This:** Skip testing API integrations within E2E tests. **Why:** Full API interaction scenarios demonstrate the entire application stack works harmoniously. ### 4.4 Accessibility Testing **Do This:** Check that the application is accessible to users with disabilities. Use tools like axe-core integrated in Cypress/Playwright. **Don't Do This:** Neglect accessibility checks in E2E tests. **Why:** To guarantee your app is inclusive and works for all users. ## 5. Tools and Libraries * **Unit Testing:** Jest, Vue Test Utils, Vue Testing Library, Vitest * **Integration Testing:** Vue Test Utils, Vue Testing Library, Cypress Component Testing, Vitest * **End-to-End Testing:** Cypress, Playwright * **Mocking:** Jest Mocks, Mocked.js, Axios Mock Adapter, Vi * **Assertion Libraries:** Jest Expect, Chai, Vitest expect * **Code Coverage:** Istanbul, c8 This document provides a solid foundation for establishing testing standards in Vue.js projects. Following these standards diligently will lead to more robust, reliable, and maintainable code.
# Core Architecture Standards for Vue.js This document outlines the recommended architectural standards for building Vue.js applications. Adhering to these guidelines promotes maintainability, scalability, testability, and overall code quality. This guide focuses on modern Vue.js (version 3 and above) practices and leverages the latest framework features and ecosystem tools. ## 1. Project Structure and Organization A well-defined project structure is crucial for navigating and maintaining large Vue.js applications. The following conventions are highly recommended. ### 1.1 Feature-Based Organization **Standard:** Organize code by feature or domain, rather than by file type (e.g., components, views, services). **Do This:** """ src/ ├── components/ # Reusable UI elements ├── features/ # Feature modules │ ├── authentication/ # Authentication feature │ │ ├── components/ # Authentication-specific components │ │ ├── services/ # Authentication services │ │ ├── views/ # Authentication views │ │ ├── composables/ # Authentication composables │ │ ├── router.js # Authentication-specific routes │ │ └── store.js # Authentication Vuex module/Pinia Store │ ├── dashboard/ # Dashboard feature │ │ └── ... ├── services/ # Global services (e.g., API client) ├── composables/ # Reusable composables (global scope) ├── views/ # Top-level views/pages ├── router/ # Vue Router configuration ├── store/ # Vuex/Pinia store configuration ├── App.vue # Root component └── main.js # Entry point """ **Don't Do This:** """ src/ ├── components/ # All components, regardless of feature ├── views/ # All views, regardless of feature ├── services/ # All services, regardless of feature └── ... """ **Why This Matters:** Feature-based organization improves code discoverability, modularity, and reduces the chances of naming conflicts. It also simplifies feature-specific testing and deployment. **Example:** Consider an e-commerce application. Instead of having a generic "components" folder, features such as "product catalog," "shopping cart," and "checkout" should be organized into their own dedicated directories under the "features" directory. ### 1.2 Component Directory Structure **Standard:** For complex components, create a dedicated directory. For simple, self-contained components, they can reside directly in the "components" directory. **Do This:** (Complex Component) """ src/ ├── components/ │ ├── ProductCard/ # Directory for a complex component │ │ ├── ProductCard.vue # Main component file │ │ ├── ProductCard.scss # Component-specific styles │ │ ├── ProductCard.spec.js # Component tests │ │ └── utils.js # Utility functions specific to the component │ ├── Button.vue # Simple, self-contained component └── ... """ **Don't Do This:** """ src/ ├── components/ │ ├── ProductCard.vue │ ├── ProductCard.scss │ ├── ProductCard.js # Mixing related files at the same level └── ... """ **Why This Matters:** Creating a directory for complex components promotes organization and encapsulation, keeping related files together. Simple components that don't require additional files can be placed directly in the "components" directory for convenience. ### 1.3 Router and Store Configuration **Standard:** Keep routing and store (Vuex or Pinia) configurations separate and modular. **Do This:** (Router Configuration) """javascript // src/router/index.js import { createRouter, createWebHistory } from 'vue-router'; import HomeView from '../views/HomeView.vue'; import AuthRoutes from '../features/authentication/router'; //Import feature-specific routes import DashboardRoutes from '../features/dashboard/router'; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: HomeView }, ...AuthRoutes, ...DashboardRoutes, ] }); export default router; """ **Do This:** (Pinia Store - Modular Approach) """javascript // src/store/index.js import { createPinia } from 'pinia'; import authStore from '../features/authentication/store'; import dashboardStore from '../features/dashboard/store'; const pinia = createPinia(); pinia.use(authStore); pinia.use(dashboardStore); export default pinia; """ **Don't Do This:** (Router - monolithic) """javascript // src/router/index.js // Long file with all routes defined here. Difficult to maintain. """ **Don't Do This:** (State Management - monolithic) """javascript // src/store/index.js // Huge state object with all modules defined within a single file """ **Why This Matters:** Modularizing routes and stores improves maintainability and scalability. Feature-specific routes and store modules reside within their respective feature directories, contributing to the overall feature-based architecture. Pinia's approach (as shown above) makes modularity much easier. ### 1.4 Asset Management **Standard:** Organize assets (images, fonts, etc.) in a dedicated "assets" directory. Distinguish between global assets and component-specific assets. **Do This:** """ src/ ├── assets/ │ ├── images/ # Global images │ │ ├── logo.png │ │ └── ... │ ├── fonts/ # Global fonts │ │ ├── OpenSans.woff2 │ │ └── ... ├── components/ │ ├── ProductCard/ │ │ ├── assets/ # Component-specific assets │ │ │ ├── placeholder.png │ │ └── ProductCard.vue └── ... """ **Don't Do This:** (Mixing global and component assets) """ src/ ├── assets/ │ ├── logo.png │ ├── placeholder.png # Confusing - is this global or component-specific? └── ... """ **Why This Matters:** A clear separation of global and component-specific assets simplifies management and reduces the risk of naming conflicts. Component-specific assets are co-located with the component, improving discoverability. ## 2. Component Architecture Vue.js excels at component-based architecture. Therefore, defining clear component patterns is crucial. ### 2.1 Single Responsibility Principle (SRP) **Standard:** Each component should have a single, well-defined responsibility. **Do This:** * Create a "ProductCard" component that displays product information. * Create a separate "AddToCartButton" component for adding the product to the cart. **Don't Do This:** * Create a single "ProductCard" component that handles product display, add-to-cart functionality, and wish list management. **Why This Matters:** SRP promotes code reusability, testability, and reduces complexity. Changes to one area of the application are less likely to impact unrelated components. ### 2.2 Composition API **Standard:** Utilize the Composition API (with "<script setup>") for improved code organization and reusability. **Do This:** """vue <template> <div> <p>Count: {{ count }}</p> <button @click="increment">Increment</button> </div> </template> <script setup> import { ref } from 'vue'; const count = ref(0); const increment = () => { count.value++; }; </script> """ **Don't Do This:** (Options API for new components) """vue <template> <div> <p>Count: {{ count }}</p> <button @click="increment">Increment</button> </div> </template> <script> export default { data() { return { count: 0 }; }, methods: { increment() { this.count++; } } }; </script> """ **Why This Matters:** The Composition API provides a more flexible and organized way to structure component logic, especially when combined with "<script setup>". It resolves many of the limitations of the Options API, particularly regarding code reuse and maintainability. "<script setup>" drastically cleans up the component definition. ### 2.3 Reusable Composables **Standard:** Extract reusable logic into composables. **Do This:** """javascript // src/composables/useCounter.js import { ref } from 'vue'; export function useCounter(initialValue = 0) { const count = ref(initialValue); const increment = () => { count.value++; }; const decrement = () => { count.value--; }; return { count, increment, decrement }; } // Component usage import { useCounter } from '@/composables/useCounter'; const { count, increment } = useCounter(10); """ **Don't Do This:** (Duplicating logic across multiple components) """vue <template> <!-- Component 1 --> count: {{ count }} <button @click="increment">Increment</button> </template> <script setup> import { ref } from 'vue'; const count = ref(0); const increment = () => { count.value++ } </script> <template> <!-- Component 2 repeats the entire pattern --> count: {{ count }} <button @click="increment">Increment</button> </template> <script setup> import { ref } from 'vue'; const count = ref(0); const increment = () => { count.value++ } </script> """ **Why This Matters:** Composables promote code reusability and reduce duplication. They encapsulate logic that can be easily shared across multiple components. This enhances maintainability and reduces the risk of introducing inconsistencies. ### 2.4 Prop Validation and Types **Standard:** Define prop types and validations for all components. **Do This:** """vue <script setup> defineProps({ title: { type: String, required: true }, price: { type: Number, default: 0, validator: (value) => value >= 0 }, product: { type: Object, required: true, validator: (obj) => { return 'name' in obj && 'description' in obj } } }); </script> """ **Don't Do This:** (Omitting prop types and validations) """vue <script setup> defineProps(['title', 'price']); // Not ideal. No type information or validation. </script> """ **Why This Matters:** Prop validation and types ensure that components receive the expected data, preventing runtime errors and improving the overall robustness of the application. They also serve as documentation for component usage. The "validator" function is particularly important for complex prop types or specific data constraints. Consider using Typescript for further type safety. ### 2.5 Emitting Custom Events **Standard:** Use "defineEmits" to declare custom events emitted by components. **Do This:** """vue <script setup> const emit = defineEmits(['addToCart', 'removeFromCart']); const addToCart = () => { emit('addToCart', { productId: 123, quantity: 1 }); }; </script> """ **Don't Do This:** (Emitting events without declaration) """vue <script setup> const addToCart = () => { this.$emit('addToCart', { productId: 123, quantity: 1 }); // Avoid using "$emit" directly. }; </script> """ **Why This Matters:** Declaring emitted events with "defineEmits" provides clarity about the component's communication interface. This improves code readability and maintainability. Declaring emitted events also helps Vue optimize event handling. ### 2.6 Dynamic Components & "is" attribute **Standard:** Utilize dynamic components with the "is" attribute when needing to switch between components at runtime based on data. Ensure that the data used to determine the component is properly sanitized to prevent potential security vulnerabilities (e.g., XSS). **Do This:** """vue <template> <component :is="currentComponent"></component> </template> <script setup> import { ref } from 'vue'; import ComponentA from './ComponentA.vue'; import ComponentB from './ComponentB.vue'; const currentComponent = ref('ComponentA'); // Function to update the component dynamically. const changeComponent = (componentName) => { if (componentName === 'ComponentA') { currentComponent.value = ComponentA; // Or the string 'ComponentA', consistent with initial value } else if (componentName === 'ComponentB') { currentComponent.value = ComponentB; // Or the string 'ComponentB' } else { console.warn('Invalid component name:', componentName); } }; // Example usage: simulating a component change setTimeout(() => { changeComponent('ComponentB'); }, 3000); </script> """ **Don't Do This:** """vue <template> <component :is="userInput"></component> // RISK: Unsanitized user input directly determining the component. </template> """ **Why This Matters:** Dynamic components using the "is" attribute offer a powerful way to render different components based on runtime data. However, it's crucial to sanitize any user-provided input used to determine the component to prevent Cross-Site Scripting (XSS) vulnerabilities. Directly binding user input to the "is" attribute is a major security risk. Whitelist acceptable components. ## 3. State Management (Pinia) Pinia is now the recommended state management library for Vue.js. ### 3.1 Define Stores with "defineStore" **Standard:** Use "defineStore" to define stores. **Do This:** """javascript // src/store/counter.js import { defineStore } from 'pinia'; export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), getters: { doubleCount: (state) => state.count * 2, }, actions: { increment() { this.count++; }, }, }); """ **Don't Do This:** (Defining stores manually) """javascript // Not recommended with Pinia const counterStore = { state: () => ({ count: 0 }), mutations: { INCREMENT: (state) => state.count++ }, actions: { increment: ({commit}) => commit('INCREMENT') }, getters: { doubleCount: (state) => state.count * 2, } } export default counterStore; """ **Why This Matters:** "defineStore" provides a type-safe and structured way to define stores. It handles the complexities of store management, such as reactivity and module registration. ### 3.2 Modular Stores **Standard:** Break down large stores into smaller, feature-specific stores. **Do This:** (See example in 1.3) **Why This Matters:** Modular stores improve maintainability and scalability, reducing the complexity of managing a single, monolithic store. Each feature can have its own dedicated store, making it easier to reason about and test. ### 3.3 Utilize Actions for Mutations **Standard:** All state modifications should be performed within actions. (Pinia conflates mutations & actions into 'actions') **Do This:** """javascript // src/store/counter.js import { defineStore } from 'pinia'; export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), actions: { increment() { this.count++; }, setCount(newCount) { this.count = newCount } }, }); """ **Don't Do This:** (Modifying state directly within components) """vue <template> <button @click="updateCount">Update Count</button> </template> <script setup> import { useCounterStore } from '../stores/counter'; const counter = useCounterStore(); const updateCount = () => { counter.count++; //BAD: avoid modifying the store state directly here } </script> """ **Why This Matters:** Actions provide a centralized place for managing state mutations. The Pinia store itself manages reactivity. Directly modifying the store from the component bypasses the centralized logging/mutation structure. ## 4. Routing Vue Router is the official routing library for Vue.js. ### 4.1 Lazy Loading Routes **Standard:** Use lazy loading for route components to improve initial page load performance. **Do This:** """javascript // src/router/index.js import { createRouter, createWebHistory } from 'vue-router'; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/about', name: 'about', component: () => import('../views/AboutView.vue') // Lazy-loaded } ] }); export default router; """ **Don't Do This:** (Importing all components upfront) """javascript // src/router/index.js import AboutView from '../views/AboutView.vue'; // Immediately loaded const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/about', name: 'about', component: AboutView } ] }); export default router; """ **Why This Matters:** Lazy loading defers the loading of route components until they are actually needed. This reduces the initial bundle size and improves the perceived performance of the application. ### 4.2 Route Meta Fields **Standard:** Use route meta fields to store additional information about a route (e.g., authentication requirements, page title). **Do This:** """javascript // src/router/index.js import { createRouter, createWebHistory } from 'vue-router'; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/profile', name: 'profile', component: () => import('../views/ProfileView.vue'), meta: { requiresAuth: true, title: 'Profile' } } ] }); """ **Why This Matters:** Route meta fields provide a way to associate additional data with a route. This data can be used for various purposes, such as implementing authentication guards, dynamic page titles or storing layout specific information. ## 5. General Coding Standards ### 5.1 Consistent Naming Conventions **Standard:** Use consistent naming conventions for all files, variables, and functions. Consider PascalCase for component names, camelCase for variables and functions, and kebab-case for file names. **Do This:** * Component: "MyComponent.vue" * Variable: "myVariable" * Function: "myFunction()" **Why This Matters:** Consistent naming conventions improve code readability and maintainability. They also help developers quickly understand the purpose of different code elements. ### 5.2 Comments and Documentation **Standard:** Comment code clearly and document components and composables using JSDoc-style comments. **Do This:** """javascript /** * Increments the counter. * * @param {number} value The amount to increment by. */ const increment = (value) => { // Increase count by specified value count.value += value; }; """ **Why This Matters:** Comments and documentation improve code understanding and facilitate collaboration. They are essential for maintaining and extending the application over time. These standards will help create maintainable, scalable, and robust Vue.js applications. Consistent application of these guidelines will improve code quality and reduce the risk of common pitfalls.
# State Management Standards for Vue.js This document outlines the standards for managing application state in Vue.js applications. Consistent state management is crucial for maintainability, scalability, and testability. It covers different approaches to state management, from simple reactive properties to more complex solutions like Vuex and Pinia. This will guide developers to write efficient, predictable, and robust Vue.js applications. ## 1. Core Principles of State Management in Vue.js ### 1.1. Single Source of Truth * **Standard:** Maintain a single source of truth for each piece of data within your application. * **Why:** Having a single source of truth ensures data consistency throughout your application. When any part of the application needs to access or modify a piece of data, it always refers to the same source, preventing inconsistencies and simplifying debugging. * **Do This:** Centralize your application's key data within Vuex stores, Pinia stores, or reactive objects, depending on the complexity of your application. * **Don't Do This:** Avoid spreading the same data across multiple components or local storage without a clear synchronization mechanism, which can lead to data inconsistencies and make debugging harder. """vue // Good: Using Pinia store import { defineStore } from 'pinia'; export const useUserStore = defineStore('user', { state: () => ({ name: 'John Doe', email: 'john.doe@example.com', }), getters: { displayName: (state) => state.name.toUpperCase(), }, actions: { updateName(newName: string) { this.name = newName; }, }, }); // Component.vue import { useUserStore } from '@/stores/user'; import { storeToRefs } from 'pinia'; import { computed } from 'vue'; export default { setup() { const userStore = useUserStore(); const { name, email } = storeToRefs(userStore); //Destructure with storeToRefs to maintain reactivity const fancyName = computed(() => userStore.displayName); return { name, email, fancyName, updateName: userStore.updateName }; }, template: " <p>Name: {{ name }}</p> <p>Email: {{ email }}</p> <p>Fancy Name: {{fancyName}}</p> <button @click="updateName('Jane Doe')">Update Name</button> ", }; // Bad: Local state duplication export default { data() { return { userName: 'John Doe' // Duplicated state, creates inconsistencies } } } """ ### 1.2. Data Flow * **Standard:** Establish a clear and predictable data flow within your application, making it easier to understand how data changes over time and where those changes originate. * **Why:** A well-defined data flow architecture is essential for maintainability and debugging, especially in large or complex applications. This ensures that changes to the application state are controlled and traceable. * **Do This:** Use unidirectional data flow, where components dispatch actions that commit mutations to the store. This makes it easy to track state changes and understand their impact on the application. * **Don't Do This:** Avoid directly modifying component props from within the component, or introducing two-way data binding without careful consideration, as this can obscure the source of state changes. """vue // Good: Unidirectional Data Flow (Pinia) //store.ts import { defineStore } from 'pinia'; export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), actions: { increment() { this.count++; }, decrement() { this.count--; }, }, }); // CounterComponent.vue import { useCounterStore } from '@/stores/counter'; export default { setup() { const counterStore = useCounterStore(); return { counterStore }; }, template: " <p>Count: {{ counterStore.count }}</p> <button @click="counterStore.increment">+</button> <button @click="counterStore.decrement">-</button> ", }; // Bad: Direct Prop Modification (Anti-Pattern) // ParentComponent.vue export default { data() { return { message: "Hello" } }, template: " <ChildComponent :message="message"/> " } // ChildComponent.vue - directly modifying the message prop. export default { props: ['message'], mounted() { // DON'T DO THIS this.message = "Goodbye" } } """ ### 1.3. Immutability * **Standard:** Treat the state as immutable whenever possible. * **Why:** Immutability simplifies debugging, enables time-travel debugging, and makes it easier to reason about the application's state. Libraries like Vuex and Pinia leverage immutability to optimize rendering and improve the overall performance of the application. * **Do This:** Use immutable data structures or libraries (like Immutable.js) if the performance benefits outweigh the complexity. When mutating state, create a new copy of the object or array instead of modifying it directly. With Pinia this is generally handled for you, but when using "reactive" or similar, this becomes important. * **Don't Do This:** Directly modify objects or arrays in the state. """vue // Good: Mutation with a new array using the spread operator import { defineStore } from 'pinia'; export const useItemsStore = defineStore('items', { state: () => ({ items: ['apple', 'banana'], }), actions: { addItem(item: string) { this.items = [...this.items, item]; // Create new array }, removeItem(index: number) { this.items = this.items.filter((_, i) => i !== index); // Create new array }, }, }); // Bad: Direct array modification (Anti-Pattern) export const useItemsStore = defineStore('items', { state: () => ({ items: ['apple', 'banana'], }), actions: { addItem(item: string) { this.items.push(item); // DO NOT DO THIS - direct mutation }, removeItem(index: number) { this.items.splice(index, 1) // DO NOT DO THIS - direct mutation } }, }); """ ### 1.4. Reactive Properties * **Standard:** Use Vue's reactivity system with "ref" and "reactive" to manage local component state. Use "computed" properties for derived state. * **Why:** Vue's reactivity system provides an efficient way to track changes in component state and automatically update the DOM. "computed" properties ensure that derived data is always up-to-date without manual intervention. * **Do This:** Prefer "ref" for primitive data types (strings, numbers, booleans) and "reactive" for objects. Use "computed" properties to derive values from reactive state. * **Don't Do This:** Directly manipulate the DOM or rely on manual updates when state changes. Do not create reactive properties at runtime outside of the setup function as it can lead to unexpected behavior. """vue // Good: Using ref and reactive with computed import { ref, reactive, computed } from 'vue'; export default { setup() { const count = ref(0); const user = reactive({ firstName: 'John', lastName: 'Doe', }); const fullName = computed(() => "${user.firstName} ${user.lastName}"); const increment = () => { count.value++; }; return { count, user, fullName, increment }; }, template: " <p>Count: {{ count }}</p> <p>Full Name: {{ fullName }}</p> <button @click="increment">Increment</button> ", }; // Bad: Manual DOM updates export default { data() { return { count: 0 } }, methods: { increment() { this.count++ document.getElementById('count').innerText = this.count //DO NOT DO THIS } }, mounted() { document.getElementById('count').innerText = this.count }, template: " <p id="count">Count: {{ count }}</p> <button @click="increment">Increment</button> ", } """ ## 2. Vuex (Legacy) vs. Pinia Note: Vuex is still viable as of Vue 3, but Pinia is now the recommended solution by the Vue team. Migrating Vuex to Pinia is recommended where possible. ### 2.1. When to Use Vuex (Legacy) * **Standard:** Use Vuex for large, complex applications where multiple components and modules need to share and manage a significant amount of state globally. * **Why:** Vuex provides a centralized store with a structured approach for managing the state of your application. It enforces a unidirectional data flow, making state changes predictable and helping you debug complex interactions. Vuex utilizes mutations for commiting state changes. * **Do This:** Utilize Vuex when you need: * Centralized state management in a large app * Time-travel debugging capabilities * Clear action/mutation flow for state modifications * **Don't Do This:** Overuse Vuex for small applications where component props and events or Pinia can handle the state management more efficiently. ### 2.2. When to Use Pinia (Recommended) * **Standard:** Prefer Pinia for most Vue.js applications due to its simpler API, better TypeScript support, and modular design. * **Why:** Pinia offers the benefits of Vuex with less boilerplate, making it easier to learn and use. It's fully typed, lightweight, and integrates seamlessly with Vue 3's composition API. Pinia stores are technically closer to components. * **Do This:** Utilize Pinia when you need: * Simple, intuitive state management * TypeScript support * Modular and composable stores * Direct access to store properties and methods * **Don't Do This:** Avoid mixing Vuex and Pinia in the same application unless there is a specific reason to do so, as this can lead to confusion and maintenance issues. If you must mix, define clear boundaries between the two systems. ### 2.3. Code Comparison: Vuex vs. Pinia """vue // Vuex Example // store/index.js import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export default new Vuex.Store({ state: { count: 0, }, mutations: { increment(state) { state.count++; }, }, actions: { increment(context) { context.commit('increment'); }, }, getters: { doubleCount(state) { return state.count * 2; }, }, }); // Component.vue import { mapState, mapActions, mapGetters } from 'vuex'; export default { computed: { ...mapState(['count']), ...mapGetters(['doubleCount']), }, methods: { ...mapActions(['increment']), }, template: " <p>Count: {{ count }}</p> <p>Double Count: {{ doubleCount }}</p> <button @click="increment">Increment</button> ", }; // Pinia Example // stores/counter.ts (or .js) import { defineStore } from 'pinia'; export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, }), getters: { doubleCount: (state) => state.count * 2, }, actions: { increment() { this.count++; }, }, }); // Component.vue import { useCounterStore } from '@/stores/counter'; import { storeToRefs } from 'pinia'; import { computed } from 'vue'; export default { setup() { const counterStore = useCounterStore(); const { count } = storeToRefs(counterStore); const doubleCount = computed(() => counterStore.doubleCount); return { count, doubleCount, increment: counterStore.increment }; }, template: " <p>Count: {{ count }}</p> <p>Double Count: {{ doubleCount }}</p> <button @click="increment">Increment</button> ", }; """ ## 3. Component Communication ### 3.1. Props and Events * **Standard:** Use props to pass data down from parent components to child components, and events to emit data or trigger actions from child components to parent components. * **Why:** Props and events provide a clear and explicit way to define the interface between components, and are essential for modular and reusable components. * **Do This:** Define props with clear types and validations. Use custom events to communicate actions from child to parent components. Use PascalCase for event names to avoid conflicts with native HTML events (e.g., "updateValue"). * **Don't Do This:** Directly modify props from within the child component. Emit native HTML events unless you have a specific reason to do so. """vue // Good: Using props and custom events // ParentComponent.vue <template> <div> <ChildComponent :message="parentMessage" @update-value="updateParentValue" /> <p>Parent Message: {{ parentMessage }}</p> </div> </template> <script> import ChildComponent from './ChildComponent.vue'; import { ref } from 'vue'; export default { components: { ChildComponent, }, setup() { const parentMessage = ref('Hello from parent'); const updateParentValue = (newValue) => { parentMessage.value = newValue; }; return { parentMessage, updateParentValue, }; }, }; </script> // ChildComponent.vue <template> <div> <p>Child Message: {{ message }}</p> <input type="text" @input="updateValue" /> </div> </template> <script> import { defineComponent } from 'vue'; export default defineComponent({ props: { message: { type: String, required: true, }, }, emits: ['update-value'], // define emitted event. setup(props, { emit }) { const updateValue = (event) => { emit('update-value', event.target.value); }; return { updateValue, }; }, }); </script> <!-- Bad: Two-way binding implementation, generally frowned upon as .sync modifier is deprecated. --> <template> <div> <p>Child Message: {{ message }}</p> <input type="text" :value="message" @input="updateValue" /> </div> </template> <script> import { defineComponent } from 'vue'; export default defineComponent({ props: { message: { type: String, required: true, }, }, emits: ['update:message'], setup(props, { emit }) { const updateValue = (event) => { emit('update:message', event.target.value); //This is the replacement for .sync but is generally not recommended as it's confusing as to the source of truth for values. }; return { updateValue, }; }, }); </script> """ ### 3.2. Provide / Inject * **Standard:** Use "provide" in a parent component to make data available to all its descendant components, regardless of how deeply nested they are. Use "inject" in a descendant component to access the provided data. * **Why:** Provide / inject simplifies passing data through multiple levels of the component tree, avoiding prop drilling and making it easier to share data between components. Using "provide" and "inject" injects the reactive connection. * **Do This:** Use provide / inject for sharing data that is used by many components across the application, such as configuration settings or global services. Provide a symbol as the injection key to avoid naming conflicts. * **Don't Do This:** Overuse provide / inject for simple component communication, as this can make it harder to track the data flow within the application. Don't provide mutable data without carefully considering the implications, as changes to the provided data will affect all injected components. """vue // Good: Provide / Inject with Symbol // ParentComponent.vue import { provide, ref } from 'vue'; const messageSymbol = Symbol('message'); export default { setup() { const message = ref('Hello from parent'); provide(messageSymbol, message); return {}; }, template: " <div> <ChildComponent /> </div> ", }; // ChildComponent.vue import { inject, readonly } from 'vue'; export default { setup() { const messageSymbol = Symbol('message'); const message = inject(messageSymbol); return { message: readonly(message) //Use readonly to prevent mutations in the child }; }, template: " <p>Message: {{ message }}</p> ", }; // Bad: Providing a mutable object directly without readonly import { provide, ref } from 'vue'; export default { setup() { const message = ref('Hello from parent'); provide('message', message); //Using the string instead of a symbol, can conflict. }, template: " <div> <ChildComponent /> </div> ", }; """ ## 4. Async State Management and Data Fetching ### 4.1. Async Actions in Pinia * **Standard:** Use async actions in Pinia to handle asynchronous operations, such as fetching data from an API or performing complex calculations. * **Why:** Async actions provide a structured way to manage asynchronous operations within your state management system, making it easier to handle loading states, errors, and data updates. * **Do This:** Define async actions within your Pinia stores to encapsulate asynchronous logic. Use "try...catch" blocks to handle errors and update the store state accordingly. * **Don't Do This:** Perform asynchronous operations directly within components without using a state management solution, as this can make it harder to track the state of the application and handle errors consistently. """vue // Good: Async Action in Pinia import { defineStore } from 'pinia'; export const useUserStore = defineStore('user', { state: () => ({ user: null, loading: false, error: null, }), actions: { async fetchUser(id: number) { this.loading = true; this.error = null; try { const response = await fetch("/api/users/${id}"); if (!response.ok) { throw new Error('Failed to fetch user'); } this.user = await response.json(); } catch (error: any) { this.error = error.message; } finally { this.loading = false; } }, }, }); // Component.vue import { useUserStore } from '@/stores/user'; import { storeToRefs } from 'pinia'; export default { setup() { const userStore = useUserStore(); const { user, loading, error } = storeToRefs(userStore); userStore.fetchUser(123); return { user, loading, error }; }, template: " <div v-if="loading">Loading...</div> <div v-if="error">Error: {{ error }}</div> <div v-if="user"> <p>Name: {{ user.name }}</p> <p>Email: {{ user.email }}</p> </div> ", }; // Bad: Fetching User inside of the component without any state tracking. export default { data() { return { user: null, loading: false, error: null } }, mounted() { this.fetchUser(123); }, methods: { async fetchUser(id) { this.loading = true; this.error = null; try { const response = await fetch("/api/users/${id}"); if (!response.ok) { throw new Error('Failed to fetch user'); } this.user = await response.json(); } catch (error) { this.error =error.message; } finally { this.loading = false; } } }, template: " <div v-if="loading">Loading...</div> <div v-if="error">Error: {{ error }}</div> <div v-if="user"> <p>Name: {{ user.name }}</p> <p>Email: {{ user.email }}</p> </div> ", } """ ### 4.2. Suspense for Data Fetching * **Standard:** Utilize the "<Suspense>" component in Vue 3 to handle asynchronous data fetching and display fallback content while data is loading. * **Why:** "<Suspense>" provides a declarative way to handle asynchronous data fetching, making it easier to manage loading states and improve the user experience. Make sure your Vue version is compatible. * **Do This:** Wrap asynchronous components within "<Suspense>" boundaries. Provide a fallback template to display while the component is loading. * **Don't Do This:** Rely on imperative loading state management within components, as this can make the code more complex and harder to maintain. """vue // Good: Using Suspense <template> <Suspense> <template #default> <AsyncComponent /> </template> <template #fallback> <div>Loading...</div> </template> </Suspense> </template> <script> import { defineAsyncComponent } from 'vue'; const AsyncComponent = defineAsyncComponent({ loader: () => import('./components/MyComponent.vue'), delay: 200, timeout: 3000, onError(error, retry, fail) { console.log(error); retry(); //fail() }, }); export default { components: { AsyncComponent, }, }; </script> // Bad: Manual loading state management import MyComponent from './components/MyComponent.vue' export default { components: { MyComponent }, template: " <MyComponent v-if="!loading" /> <p v-else>Loading</p> ", data() { return { loading: true } }, async mounted() { //Simulated example await new Promise(resolve => setTimeout(resolve, 1000)) this.loading = false } } """ ## 5. Testing State Management ### 5.1. Unit Testing Pinia Stores * **Standard:** Write unit tests to verify the behavior of your Pinia stores, including state mutations, actions, and getters. * **Why:** Unit tests ensure that your state management logic is working correctly and prevent regressions when you make changes to the code. * **Do This:** Use testing frameworks like Jest or Mocha, along with testing utilities like "@vue/test-utils", to write unit tests for your Pinia stores. Mock external dependencies, such as API calls, to isolate the store's behavior. * **Don't Do This:** Skip testing your state management logic, as this can lead to bugs and unexpected behavior in your application. """javascript // Good: Unit Testing Pinia Store (Jest) import { createPinia, setActivePinia } from 'pinia'; import { useCounterStore } from '@/stores/counter'; import { beforeEach, describe, expect, it } from 'vitest'; describe('Counter Store', () => { beforeEach(() => { setActivePinia(createPinia()); }); it('should increment the count', () => { const counterStore = useCounterStore(); expect(counterStore.count).toBe(0); counterStore.increment(); expect(counterStore.count).toBe(1); }); it('should decrement the count', () => { const counterStore = useCounterStore(); expect(counterStore.count).toBe(0); counterStore.decrement(); expect(counterStore.count).toBe(-1); }); it('should double the count', () => { const counterStore = useCounterStore(); counterStore.count = 5; expect(counterStore.doubleCount).toBe(10); }); }); // Bad: Not testing """ ### 5.2. End-to-End Testing with State Management * **Standard:** Incorporate state management into your end-to-end tests to verify that the application state is being updated correctly in response to user interactions. * **Why:** End-to-end tests ensure that the entire application, including the state management layer, is working correctly and providing a consistent user experience. * **Do This:** Use end-to-end testing frameworks like Cypress or Playwright to simulate user interactions and verify that the state of the application is being updated as expected. * **Don't Do This:** Only test individual components without considering the overall state of the application, as this can miss important integration issues.