Table of Contents#
- Understanding Vuex Modules
- The Problem: Duplicated Functions Across Modules
- Best Practices to Share Common Functions
- Practical Implementation Example
- Common Pitfalls to Avoid
- Conclusion
- References
1. Understanding Vuex Modules#
Before diving into sharing functions, let’s recap how Vuex modules work. A Vuex module is a plain JavaScript object that can contain state, getters, mutations, actions, and even nested modules. By default, modules are namespaced (if namespaced: true is set), meaning their getters, mutations, and actions are scoped to the module, preventing naming collisions.
Example of a basic Vuex module:
// store/modules/posts.js
export default {
namespaced: true,
state: () => ({
items: [],
loading: false,
error: null
}),
getters: {
allPosts: (state) => state.items
},
mutations: {
setPosts(state, posts) {
state.items = posts;
},
setLoading(state, isLoading) {
state.loading = isLoading;
}
},
actions: {
async fetchPosts({ commit }) {
commit('setLoading', true);
try {
const response = await fetch('/api/posts'); // Duplicated logic!
const posts = await response.json();
commit('setPosts', posts);
} catch (err) {
commit('setError', err.message);
} finally {
commit('setLoading', false);
}
}
}
};This module manages posts, but if we had a comments module, it might have a nearly identical fetchComments action with the same API-fetching logic—leading to duplication.
2. The Problem: Duplicated Functions Across Modules#
As applications grow, modules often require similar helper functions. For example:
- API request logic (e.g., adding auth headers, handling errors).
- Data formatting (e.g., converting dates, sanitizing inputs).
- Validation (e.g., checking required fields).
- Common mutations (e.g.,
setLoading,setError).
Duplicating these functions across modules violates the DRY principle, making:
- Code harder to update (changing logic requires edits in multiple places).
- Testing cumbersome (tests must be repeated for each module).
- Debugging tricky (bugs may appear in multiple duplicated instances).
3. Best Practices to Share Common Functions#
Let’s explore proven strategies to share functions across Vuex modules, along with their use cases and tradeoffs.
3.1 Utility/Helper Files#
What it is: Extract common functions into standalone utility files (e.g., utils/api.js, utils/formatters.js) and import them into modules.
Why use it: Simple, decoupled, and easy to test. Utilities live outside Vuex, so they can be reused across components, plugins, or even other parts of the app.
Example:
Create a utility for API requests with error handling and auth headers:
// src/utils/api.js
import axios from 'axios'; // Use axios for better request handling
const apiClient = axios.create({
baseURL: process.env.VUE_APP_API_URL,
headers: {
'Content-Type': 'application/json'
}
});
// Add auth token to requests (if available)
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('authToken');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Handle common errors (e.g., 401 Unauthorized)
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Redirect to login
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// Reusable fetch function with loading state management
export const fetchWithLoading = async (url, commit) => {
commit('setLoading', true);
try {
const response = await apiClient.get(url);
return response.data;
} catch (err) {
commit('setError', err.message || 'Failed to fetch data');
throw err; // Re-throw to allow module-specific handling
} finally {
commit('setLoading', false);
}
};Now import fetchWithLoading into your modules to eliminate duplication:
// store/modules/posts.js
import { fetchWithLoading } from '@/utils/api';
export default {
namespaced: true,
state: () => ({ items: [], loading: false, error: null }),
mutations: {
setPosts(state, posts) { state.items = posts; },
setLoading(state, isLoading) { state.loading = isLoading; },
setError(state, error) { state.error = error; }
},
actions: {
async fetchPosts({ commit }) {
const posts = await fetchWithLoading('/posts', commit); // Reuse utility
commit('setPosts', posts);
}
}
};Comments:
- Best for stateless, pure functions (no dependency on Vuex state).
- Utilities can be tested independently of Vuex.
- Avoid including Vuex-specific logic (e.g.,
commit) in utilities unless necessary (as in the example above).
3.2 Module Composition with Base/Shared Modules#
What it is: Extract common module logic (e.g., state, mutations, actions) into a "base" module, then extend it with module-specific code.
Why use it: Ideal for modules with similar CRUD operations (e.g., users, posts, comments), where most logic is shared but some state/getters are unique.
Example:
Create a baseCrudModule with common CRUD actions:
// store/modules/baseCrudModule.js
export default {
namespaced: true,
state: () => ({
items: [],
loading: false,
error: null,
resource: null // To be set by child modules
}),
mutations: {
setItems(state, items) { state.items = items; },
setLoading(state, isLoading) { state.loading = isLoading; },
setError(state, error) { state.error = error; }
},
actions: {
async fetchAll({ state, commit }) {
if (!state.resource) throw new Error('Resource not defined');
commit('setLoading', true);
try {
const response = await fetch(`/api/${state.resource}`);
const items = await response.json();
commit('setItems', items);
} catch (err) {
commit('setError', err.message);
} finally {
commit('setLoading', false);
}
}
// Add other shared actions: create, update, delete...
}
};Extend the base module to create a posts module:
// store/modules/posts.js
import baseCrudModule from './baseCrudModule';
export default {
...baseCrudModule, // Spread base module
state: () => ({
...baseCrudModule.state(), // Merge base state
resource: 'posts', // Set module-specific resource
featured: [] // Add module-specific state
}),
getters: {
...baseCrudModule.getters, // Inherit base getters (if any)
featuredPosts: (state) => state.items.filter(post => post.isFeatured) // Module-specific getter
}
// Override actions/mutations if needed
};Comments:
- Use
Object.assignor the spread operator (...) to merge base and child module options. - Avoid deep merging issues (e.g., nested
stateobjects) by using functions for state (Vuex best practice). - Best for modules with 80% shared logic and 20% unique logic.
3.3 Cross-Module Actions for Reusable Logic#
What it is: Call an action from one module in another module using dispatch, allowing reuse of the source module’s logic.
Why use it: Useful when a function is tightly coupled to a specific module’s state (e.g., a notification module that handles toast messages for multiple modules).
Example:
A notificationModule with a showToast action:
// store/modules/notificationModule.js
export default {
namespaced: true,
actions: {
showToast({ commit }, { message, type }) {
// Logic to show a toast (e.g., using a Vue component or library like Vuetify)
console.log(`[${type}]: ${message}`);
}
}
};Other modules can dispatch notificationModule/showToast:
// store/modules/posts.js
export default {
namespaced: true,
actions: {
async fetchPosts({ commit, dispatch }) {
commit('setLoading', true);
try {
const response = await fetch('/api/posts');
const posts = await response.json();
commit('setPosts', posts);
dispatch('notificationModule/showToast', {
message: 'Posts loaded!',
type: 'success'
}, { root: true }); // { root: true } required for namespaced modules
} catch (err) {
commit('setError', err.message);
dispatch('notificationModule/showToast', {
message: err.message,
type: 'error'
}, { root: true });
} finally {
commit('setLoading', false);
}
}
}
};Comments:
- Use
{ root: true }indispatchto call actions from namespaced modules. - Caution: Tightly couples modules (e.g.,
postsdepends onnotificationModule). Use sparingly to avoid brittle code.
3.4 Vuex Plugins for Global Helpers#
What it is: Use Vuex plugins to inject global helper functions into the store or modules. Plugins run when the store is initialized and can attach utilities to store or store._vm (Vue instance).
Why use it: For functions that need access to the store (e.g., logging, analytics) or should be available globally to all modules.
Example:
Create a plugin to add a logAction helper:
// store/plugins/loggerPlugin.js
export default function loggerPlugin(store) {
// Attach helper to store
store.logAction = (moduleName, actionName, data) => {
console.log(`[${moduleName}/${actionName}]`, data);
};
// Or inject into all modules via store._vm (Vue instance)
store._vm.$log = (message) => console.log('[GLOBAL LOG]', message);
}Register the plugin when creating the store:
// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import loggerPlugin from './plugins/loggerPlugin';
import postsModule from './modules/posts';
Vue.use(Vuex);
export default new Vuex.Store({
modules: { posts: postsModule },
plugins: [loggerPlugin] // Register plugin
});Use the helper in a module:
// store/modules/posts.js
export default {
namespaced: true,
actions: {
async fetchPosts({ commit, rootState }) {
rootState.logAction('posts', 'fetchPosts', 'Fetching posts...'); // Use store.logAction
// ... rest of logic
}
}
};Comments:
- Best for cross-cutting concerns (logging, error tracking).
- Avoid overusing plugins—they can make code harder to trace.
store._vmis a private API; use with caution (may change between versions).
3.5 "Mixins" for Module Factory Functions#
What it is: Use factory functions to generate module objects with shared logic. Similar to mixins, but for Vuex modules (since Vuex modules are plain objects, not Vue instances).
Why use it: Flexible way to inject shared actions/mutations into multiple modules without inheritance.
Example:
Create a factory function that adds common setLoading and setError mutations:
// store/mixins/withLoading.js
export default function withLoading(moduleOptions = {}) {
return {
...moduleOptions,
state: () => ({
...(moduleOptions.state ? moduleOptions.state() : {}),
loading: false,
error: null
}),
mutations: {
...moduleOptions.mutations,
setLoading(state, isLoading) {
state.loading = isLoading;
},
setError(state, error) {
state.error = error;
}
}
};
}Use the factory to create a module:
// store/modules/posts.js
import withLoading from '../mixins/withLoading';
export default withLoading({
namespaced: true,
state: () => ({ items: [] }), // Merged with loading/error from withLoading
actions: {
async fetchPosts({ commit }) {
commit('setLoading', true);
// ... fetch logic
}
}
});Comments:
- More flexible than base modules for ad-hoc shared logic.
- Use to add specific features (e.g.,
withLoading,withPagination) to modules. - Ensure state/mutations are merged correctly (use functions for
stateto avoid references).
4. Practical Implementation Example#
Let’s tie it all together with a real-world scenario:
Goal: Share API fetch logic and loading state across posts and comments modules.
Step 1: Create API Utility#
// src/utils/api.js
import axios from 'axios';
const api = axios.create({ baseURL: '/api' });
// Add auth headers
api.interceptors.request.use(config => {
config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;
return config;
});
// Shared fetch function with loading/error handling
export const fetchResource = async (url, commit) => {
commit('setLoading', true);
try {
const { data } = await api.get(url);
return data;
} catch (err) {
commit('setError', err.response?.data?.message || 'Error');
throw err;
} finally {
commit('setLoading', false);
}
};Step 2: Create a Loading Mixin#
// src/store/mixins/withLoading.js
export default (module) => ({
...module,
state: () => ({
...module.state(),
loading: false,
error: null
}),
mutations: {
...module.mutations,
setLoading(state, isLoading) { state.loading = isLoading; },
setError(state, error) { state.error = error; }
}
});Step 3: Build Modules with Shared Logic#
// src/store/modules/posts.js
import withLoading from '../mixins/withLoading';
import { fetchResource } from '@/utils/api';
export default withLoading({
namespaced: true,
state: () => ({ items: [] }),
mutations: {
setPosts(state, posts) { state.items = posts; }
},
actions: {
async fetchPosts({ commit }) {
const posts = await fetchResource('/posts', commit);
commit('setPosts', posts);
}
}
});// src/store/modules/comments.js
import withLoading from '../mixins/withLoading';
import { fetchResource } from '@/utils/api';
export default withLoading({
namespaced: true,
state: () => ({ items: [] }),
mutations: {
setComments(state, comments) { state.items = comments; }
},
actions: {
async fetchComments({ commit }) {
const comments = await fetchResource('/comments', commit);
commit('setComments', comments);
}
}
});Result: Both modules share loading/error state, setLoading/setError mutations, and fetchResource logic—no duplication!
5. Common Pitfalls to Avoid#
- Mutating State Outside Mutations: Even in shared utilities, always use
committo mutate state (never modifystatedirectly). - Over-Coupling Modules: Avoid excessive cross-module actions (e.g.,
dispatch('otherModule/action')). Prefer utilities or base modules for loose coupling. - Ignoring Namespacing: Always set
namespaced: trueto avoid naming collisions when sharing actions/mutations. - Bloating Utilities with Vuex Logic: Keep utilities focused on pure logic; avoid hardcoding
commitordispatchunless necessary. - Forgetting to Test Shared Code: Utilities and base modules need tests too!
6. Conclusion#
Sharing common functions across Vuex modules is critical for maintaining a clean, scalable codebase. The best approach depends on your use case:
- Utilities: For stateless, pure functions (API calls, formatting).
- Base Modules: For CRUD-like modules with shared state/actions.
- Cross-Module Actions: For one-off reuse of module-specific logic (use sparingly).
- Plugins: For global cross-cutting concerns (logging, analytics).
- Factory Functions: For flexible injection of shared mutations/actions.
By adopting these practices, you’ll reduce duplication, improve maintainability, and make your Vuex store easier to debug and extend.