javascriptroom blog

Vuex: How to Share Common Functions Across Modules in Vue.js 2 - Best Practices

Vuex is Vue.js’s official state management library, designed to centralize and manage application state in large-scale Vue.js applications. A key feature of Vuex is modules—a way to split the store into smaller, reusable pieces, each handling a specific domain of your application (e.g., user, posts, cart). While modules promote organization, they often lead to a common problem: code duplication. Functions like API calls, data validation, or formatting logic are frequently repeated across modules, making the codebase harder to maintain, test, and debug.

In this blog, we’ll explore best practices to share common functions across Vuex modules in Vue.js 2 (using Vuex 3, the compatible version for Vue 2). We’ll cover practical techniques, implementation examples, and pitfalls to avoid, ensuring your Vuex store remains clean, DRY (Don’t Repeat Yourself), and scalable.

2026-02

Table of Contents#

  1. Understanding Vuex Modules
  2. The Problem: Duplicated Functions Across Modules
  3. Best Practices to Share Common Functions
  4. Practical Implementation Example
  5. Common Pitfalls to Avoid
  6. Conclusion
  7. 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.assign or the spread operator (...) to merge base and child module options.
  • Avoid deep merging issues (e.g., nested state objects) 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 } in dispatch to call actions from namespaced modules.
  • Caution: Tightly couples modules (e.g., posts depends on notificationModule). 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._vm is 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 state to 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 commit to mutate state (never modify state directly).
  • 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: true to avoid naming collisions when sharing actions/mutations.
  • Bloating Utilities with Vuex Logic: Keep utilities focused on pure logic; avoid hardcoding commit or dispatch unless 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.

7. References#