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();.