Skip to main content

Overview

The application uses Pinia as the state management solution for Vue 3. Pinia provides a lightweight, type-safe store with a simple API and excellent DevTools integration.

Setup

Pinia Initialization

Pinia is initialized in src/main.js:2:
src/main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import './style.css';

const app = createApp(App);
const pinia = createPinia();

app.use(pinia);  // Install Pinia before router
app.use(router);

app.mount('#app');
Pinia is installed before the router to ensure stores are available for navigation guards.

Store Architecture

The breadcrumbs store manages navigation breadcrumb state across the application.
src/store/breadCrumbs.js
import { defineStore } from 'pinia';

export const useBreadCrumbsStore = defineStore('breadCrumbs', {
  state: () => ({
    breadCrumbs: [],
  }),
  actions: {
    setBreadCrumbs(newBreadCrumbs) {
      this.breadCrumbs = newBreadCrumbs;
    }
  }
});

State Management Patterns

Local Storage for Authentication

While Pinia is available, the application uses localStorage for authentication state to persist across page refreshes.
Storage Format:
{
  nombre: "John Doe",
  rol: "Administrador",
  autenticado: true
}
Reading from localStorage:
src/components/Header.vue
const { nombre, rol } = JSON.parse(localStorage.getItem('usuario'));
Router Guard with localStorage:
src/router/index.js
router.beforeEach((to, from, next) => {
  const usuario = JSON.parse(localStorage.getItem('usuario'));
  
  // Check authentication
  if (to.meta.autenticado && !usuario.autenticado) {
    return next('/');
  }
  
  // Redirect if already authenticated
  if (to.path === '/' && usuario && usuario.autenticado) {
    return next('/inicio');
  }
  
  // Check admin role
  if (to.meta.administrador && usuario.rol !== 'Administrador') {
    return next('/inicio');
  }
  
  next();
});

Service Layer Pattern

The application uses a service layer pattern for data management instead of Pinia stores for CRUD operations.
src/services/productos.service.js
import api from '../api/axios.js';

export default {
  async listar() {
    const res = await api.get('/productos');
    const json = res.data;
    return json.data; 
  },

  async crear(producto) {
    const res = await api.post('/productos', producto);
    return res.data;
  },

  async leer(id) {
    const res = await api.get(`/productos/${id}`);
    const json = res.data;
    return json.data;
  },

  async actualizar(producto) {
    const res = await api.put(`/productos/${producto.id}`, producto);
    return res.data;
  },

  async eliminar(id) {
    const res = await api.delete(`/productos/${id}`);
    return res.data;
  }
}
Design Decision: The application uses component-local ref() state for data management rather than centralized Pinia stores. This approach works well for CRUD operations where data is fetched fresh on each page load.

Component State Patterns

Reactive State with ref()

Components manage local state using Vue’s ref() composition API:
const modalVer = ref(null);
const modalRegistro = ref(null);
const modalActualizar = ref(null);
const modalEliminar = ref(null);

function mostrarModalRegistrar() {
  modalRegistro.value.mostrarModal();
}

Computed Properties

From src/components/Tabla.vue:82:
import { ref, computed, watch } from 'vue';

const dataFiltrada = computed(() => {
  if(!props.busqueda) return props.data;
  const busquedaFormateada = props.busqueda.toLowerCase();
  return props.data.filter(item => {
    return props.columnas.some(columna => {
      const valor = item[columna.field];
      return valor && valor.toString().toLowerCase().includes(busquedaFormateada);
    });
  });
});

const totalPaginas = computed(() => {
  return Math.ceil(dataFiltrada.value.length / itemsPorPagina.value);
});

const dataPaginada = computed(() => {
  const inicio = (paginaActual.value - 1) * itemsPorPagina.value;
  const fin = inicio + itemsPorPagina.value;
  return dataFiltrada.value.slice(inicio, fin);
});

Watchers

src/components/Tabla.vue
watch([itemsPorPagina, busqueda], () => {
  paginaActual.value = 1;
});

Provide/Inject Pattern

Notification System

From src/components/LoginFormulario.vue:45:
import { ref, inject } from 'vue';

const mostrarNotificacion = inject('triggerNotificacion');

// Later in the component
if (usuario.value.nombre === '' || usuario.value.contrasena === '') {
  mostrarNotificacion('error', 'Complete ambos campos.');
  return;
}
The application likely provides notification functionality at the layout or app level:
import { provide } from 'vue';

// In parent component
const triggerNotificacion = (tipo, mensaje) => {
  notificacionRef.value.agregarNotificacion(tipo, mensaje);
};

provide('triggerNotificacion', triggerNotificacion);
This allows any child component to trigger notifications without prop drilling.

Context Injection

From src/components/BotonTabla.vue:29:
const origen = inject('origen', '');

// Used to conditionally show "Detalles" button
<li v-if="origen !== 'Usuarios'">
  <button type="button" @click="$emit('ver')">Detalles</button>
</li>

State Management Best Practices

Centralized Navigation

Use Pinia stores for global UI state like breadcrumbs

Local Data State

Keep CRUD data in component-local refs for simplicity

Service Layer

Encapsulate API calls in service modules

Persistent Auth

Use localStorage for authentication that survives refreshes

When to Use Pinia vs Local State

  • State needs to be shared across multiple unrelated components
  • State needs to persist across route changes
  • Complex state with multiple actions and getters
  • DevTools debugging is important
Examples:
  • Breadcrumb navigation state
  • Global notification queue
  • User preferences and settings
  • Application-wide filters

State Access Patterns

Using storeToRefs

Extract reactive references from stores:
import { storeToRefs } from 'pinia';
import { useBreadCrumbsStore } from '../store/breadCrumbs.js';

const breadCrumbsStore = useBreadCrumbsStore();
const { breadCrumbs } = storeToRefs(breadCrumbsStore);
// breadCrumbs is now a reactive ref
Always use storeToRefs() when destructuring state to maintain reactivity. Direct destructuring will lose reactivity.

Direct Store Access

Call actions directly on the store instance:
const breadCrumbsStore = useBreadCrumbsStore();
breadCrumbsStore.setBreadCrumbs([
  { nombre: 'Productos', ruta: '/productos' }
]);

Future Store Expansion

The current architecture can be extended with additional Pinia stores:
import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  state: () => ({
    currentUser: null,
    isAuthenticated: false,
  }),
  getters: {
    isAdmin: (state) => state.currentUser?.rol === 'Administrador',
  },
  actions: {
    setUser(user) {
      this.currentUser = user;
      this.isAuthenticated = true;
      localStorage.setItem('usuario', JSON.stringify(user));
    },
    logout() {
      this.currentUser = null;
      this.isAuthenticated = false;
      localStorage.removeItem('usuario');
    }
  }
});

Summary

The Sistema de Productos uses a hybrid state management approach:
  • Pinia for global UI state (breadcrumbs, potentially user session)
  • localStorage for persistent authentication data
  • Component-local refs for page-specific data and UI state
  • Service layer for API communication and data fetching
  • Provide/Inject for cross-cutting concerns like notifications
This approach balances simplicity with scalability, keeping the codebase maintainable while allowing for future expansion.

Build docs developers (and LLMs) love