Skip to content

State management

Local vs global state

  • Local State:
  • Defined and managed inside a Vue component using ref, reactive, or the Composition API.
  • Not accessible outside the component.
  • Examples: form inputs, toggle states, dialog visibility within a single component.

  • Global State:

  • Centralized state defined in a store (e.g., Pinia or singleton object).
  • Shared across multiple components or modules.
  • Ensures a single source of truth for application-wide data.
  • Examples: user session, theme, menu state, global dialogs.

Global state patterns (stores)

There are two approaches for global state:

1. Pinia store (Vue app scoped)

Use when state is shared between components in a single Vue application.

Example: easy-ai/assistant/store/menu.ts

import { defineStore } from "pinia";
import { ref, readonly } from "vue";
import { MenuType } from "../types/menu";

export const useMenuStore = defineStore("easyAIMenu", () => {
  const menuOpened = ref(false);
  const menuType = ref<MenuType | null>(null);

  const setMenuOpened = (value: boolean) => {
    menuOpened.value = value;
  };

  const setMenuType = (type: MenuType) => {
    menuType.value = type;
  };

  return {
    menuOpened: readonly(menuOpened),
    menuType: readonly(menuType),
    setMenuOpened,
    setMenuType,
  };
});

Usage

import { useMenuStore } from "@/src/easy-ai/assistant/store/menu";

const menuStore = useMenuStore();
menuStore.setMenuOpened(true);

Use it for UI state, shared data and coordination within one Vue app.

2. Singleton store (framework-agnostic)

Use when state must be accessible across separately mounted Vue apps or from vanilla JS.

Example: easy-ui/store/easyDialogStore.ts

class EasyDialogStore {
  private static instance: EasyDialogStore = new EasyDialogStore();
  private dialogStates: Record<string, { isOpened: boolean; hideBodyScrollbar: boolean }> = {};

  private constructor() {}

  static getInstance(): EasyDialogStore {
    return EasyDialogStore.instance;
  }

  public registerNewDialog({ isOpened, hideBodyScrollbar = true }: { isOpened: boolean; hideBodyScrollbar?: boolean }): string {
    const dialogId = this.generateNewDialogId();
    this.dialogStates[dialogId] = { isOpened, hideBodyScrollbar };
    this.processHideBodyScrollbar(dialogId);
    return dialogId;
  }

  public updateDialogOpened(dialogId: string, opened: boolean): void {
    if (!this.dialogStates[dialogId]) throw new Error(`Dialog with ID ${dialogId} not found`);
    this.dialogStates[dialogId].isOpened = opened;
    this.processHideBodyScrollbar(dialogId);
  }
}

export default EasyDialogStore.getInstance();

Usage

import easyDialogStore from "@/src/easy-ui/store/easyDialogStore";

const dialogId = easyDialogStore.registerNewDialog({ isOpened: true });
easyDialogStore.updateDialogOpened(dialogId, false);

Use it for shared concerns (dialogs, overlays) across apps or in non-Vue contexts.

Best practices

  • Use Pinia for state shared between Vue components.
  • Use singleton stores for cross-app logic.
  • Use stores instead of prop/emit drilling.
  • Change state only through methods or actions.
  • Expose state with readonly() to prevent mutation outside the store.
  • One concern per store.
  • Do not use storeToRefs; work with the store through a named constant, const menuStore = useMenuStore();.