Skip to content

Stores

Zustand stores provide global state. All stores except useHoneyframeStore use localStorage persistence via zustand/middleware.

Barrel export: src/stores/index.ts

ts
import { useAuthStore, useThemeStore, useLanguageStore, useHoneyframeStore } from '@/stores';

useAuthStore

File: src/stores/useAuthStore.ts Storage key: hermina360-auth Persisted: yes (localStorage)

ts
interface AuthState {
  user: AuthUser | null;
  accessToken: string | null;
  isLoggedIn: boolean;

  setAuthData(token: string, user: AuthUser): void;
  setAccessToken(token: string): void;
  setUser(user: AuthUser): void;
  clearAuthData(): void;
  getUser(): AuthUser | null;
}
ActionWhen to use
setAuthData(token, user)Login success — sets token, user, and isLoggedIn: true
clearAuthData()Logout or 401 — clears everything, isLoggedIn: false
setAccessToken(token)Token refresh
setUser(user)Profile update
getUser()Non-reactive access (inside callbacks)

Auto-used by: restApi request interceptor injects Authorization: Bearer {accessToken} on every request. Response interceptor calls clearAuthData() + redirects on 401.

ts
// Login
const { setAuthData } = useAuthStore();
setAuthData(response.accessToken, response.user);

// Check auth
const isLoggedIn = useAuthStore(s => s.isLoggedIn);

// Logout
useAuthStore.getState().clearAuthData();

Partialize: only user, accessToken, isLoggedIn are persisted.


useThemeStore

File: src/stores/useThemeStore.ts Storage key: hermina360-theme Persisted: yes (localStorage)

ts
interface ThemeState {
  mode: 'light' | 'dark';
  toggle(): void;
  setMode(mode: ThemeMode): void;
}

toggle() and setMode() also call document.documentElement.setAttribute('data-theme', mode) for CSS targeting. On hydration, onRehydrateStorage syncs the DOM attribute.

ts
const { mode, toggle } = useThemeStore();
// mode === 'light' | 'dark'

useLanguageStore

File: src/stores/useLanguageStore.ts Storage key: hermina360-lang Persisted: yes (localStorage)

ts
interface LanguageState {
  lang: SupportedLang;        // 'id' | 'en'
  setLang(lang: SupportedLang): void;
}

setLang() also lazy-imports @/lib/i18n and calls i18n.changeLanguage(lang) to sync react-i18next. Same sync happens on hydration via onRehydrateStorage.

Default: DEFAULT_LANG from @/lib/i18n.

ts
const { lang, setLang } = useLanguageStore();
setLang('en');

useHoneyframeStore

File: src/stores/useHoneyframeStore.ts Persisted: no (in-memory only, re-fetched on page load)

ts
interface HoneyframeState {
  apps: Record<string, HFWebappResponse>;
  setApp(appKey: string, data: HFWebappResponse): void;
}

Stores Honeyframe webapp configs keyed by app key (e.g. 'hermina360', 'hermina360_financial').

Populated by: useWebappConfig(appKey) hook — fetches config from API and calls setApp().

ts
// Access config for an app
const app = useHoneyframeStore(s => s.apps['hermina360']);
const pages = app?.config.pages;
const assetId = app?.asset_id;

// Find a specific page
const execPage = app?.config.pages.find(p => p.key === 'executive');

Why not persisted: Config is cheap to fetch and cached by React Query (staleTime: Infinity). Avoids stale data issues when HF config is republished.


Adding a New Store

  1. Create src/stores/useMyStore.ts:
ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

interface MyState {
  value: string | null;
  setValue(v: string | null): void;
}

export const useMyStore = create<MyState>()(
  persist(
    (set) => ({
      value: null,
      setValue: (v) => set({ value: v }),
    }),
    {
      name: 'hermina360-mystore',          // localStorage key
      storage: createJSONStorage(() => localStorage),
    },
  ),
);
  1. Export from src/stores/index.ts:
ts
export { useMyStore } from './useMyStore';

Convention: storage keys prefixed with hermina360- to avoid collisions.