Appearance
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;
}| Action | When 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
- 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),
},
),
);- Export from
src/stores/index.ts:
ts
export { useMyStore } from './useMyStore';Convention: storage keys prefixed with hermina360- to avoid collisions.