State Management

TanStack Query for server state, Zustand for global UI state, and local useState when to use each.

SCRIPE uses three state management tools, each for a specific category: TanStack Query v5 for server/API data, Zustand for global UI state (auth, sidebar, theme), and React's useState for local component state.

State Categories

State Management Architecture
Server State (TanStack Query v5) — API data: admins, users, tenants, roles, etc.
Global UI State (Zustand) — Auth, sidebar, theme, toasts
Local Component State (useState) — Form inputs, modals, toggles
Language State (LanguageProvider) — Arabic/English, RTL/LTR, translations
State TypeToolLocationPersistence
Server DataTanStack Query v5ViewModelsIn-memory cache (configurable staleTime)
Auth/SessionZustand + persist@core/storelocalStorage
Theme/SidebarZustand@core/storeOptional
ToastsZustand@core/storeNone
LanguageLanguageProvider@core/providerslocalStorage
Form InputsuseState / React Hook FormComponentsNone
Modal/ToggleuseStateComponentsNone

TanStack Query (Server State)

TanStack Query handles all API data fetching, caching, background refetching, mutations, optimistic updates, and pagination. It's used exclusively in ViewModels, never in Views or Components.

Query Key Factory PatternTypeScript
// Consistent query key structure for cache management
/**
 * Exported constant defining parameters and fields for admin keys configurations.
 */
export const adminKeys = {
  all:     ["admins"] as const,
  lists:   ()           => [...adminKeys.all, "list"] as const,
  list:    (filters: F)  => [...adminKeys.lists(), filters] as const,
  details: ()           => [...adminKeys.all, "detail"] as const,
  detail:  (id: string) => [...adminKeys.details(), id] as const,
};

// Usage in ViewModel
const { data, isLoading } = useQuery({
  queryKey: adminKeys.list({ page, search, sortBy, sortDir }),
  queryFn: () => adminRepo.getAll({ page, search, sortBy, sortDir }),
  staleTime: 2 * 60 * 1000,  // 2 min
});

// Invalidation after mutation
const queryClient = useQueryClient();
queryClient.invalidateQueries({ queryKey: adminKeys.all });
query-client.ts — Global Query Cache ConfigurationTypeScript
import { QueryClient } from "@tanstack/react-query";

/**
 * Documentation for QueryClient
 */
export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 2 * 60 * 1000,    // 2 minutes query cache freshness
      refetchOnWindowFocus: false, // Prevents background query spam
      retry: 1,                    // Fail fast on network interruption
    },
  },
});

Mutations & Cache Invalidation

Mutation Pattern with Cache InvalidationTypeScript
export function useCreateAdmin() {
  const queryClient = useQueryClient();
  const repo = container.adminRepository;
  const { t } = Language();

  return useMutation({
    mutationFn: async (data: CreateAdminInput) => {
      const result = await repo.create(data);
      if (result.isErr()) throw result.error;
      return result.value;
    },
    onSuccess: () => {
      // Invalidate all admin queries to refetch
      queryClient.invalidateQueries({ queryKey: adminKeys.all });
      toast.success(t("admins.createSuccess"));
    },
    onError: (error) => {
      toast.error(error.message);
    },
  });
}

Zustand (Global UI State)

Zustand stores manage UI state that needs to be shared across components: authentication state (useAuthStore), sidebar/theme (useScripetore), and toast notifications (useToastStore).

StorePurposePersisted?
useAppStoreUser session, permissions, roles, sidebar status, density setting, tenant contextYes (localStorage, partialized)
useCurrencyPreferencePreferred display currency and converting rulesYes (only display mode setting)
useToastStoreToast notification queue (auto-dismiss)No
useAppStore.ts — Core InterfaceTypeScript
interface AppState {
  // Sidebar State
  sidebarOpen: boolean;
  toggleSidebar: () => void;
  setSidebarOpen: (open: boolean) => void;

  // UI Density
  density: "compact" | "comfortable" | "spacious";
  setDensity: (density: "compact" | "comfortable" | "spacious") => void;

  // Auth State
  user: User | null;
  isAuthenticated: boolean;
  permissions: PermissionCode[];
  roles: AdminRole[];
  restrictedFields: Record<string, string[]>;
  setAuth: (user: User, permissions: PermissionCode[], roles: AdminRole[], isFreshLogin?: boolean) => void;
  logout: () => void;

  // Tenant & Subscription Info
  tenantCode: string | null;
  setTenantCode: (code: string | null) => void;
  editionName: string | null;
  subscriptionStatus: string | null;

  // Hydration State
  _hasHydrated: boolean;
  setHasHydrated: (state: boolean) => void;
}

Localization State

Language preferences are managed through the LanguageProvider context. It persists the selected language in localStorage and provides the t() function for translations throughout the app.

Language() Hook — APITypeScript
const {
  language,      // 'en' | 'ar'
  direction,     // 'ltr' | 'rtl'
  setLanguage,   // (lang: 'en' | 'ar') => void
  t,             // (key: string, vars?: Record<string, string>) => string
} = Language();

// Usage:
t('common.save');                           // "Save"
t('errors.minLength', { min: '8' });        // "Must be at least 8 characters"

// RTL-aware styling:
<div className={direction === 'rtl' ? 'text-right' : 'text-left'}>

Anti-Patterns

  • Don't place UI business logic or async mutations directly inside React View components.

    • Store API data in Zustand (useEffect + fetch → Zustand setState)
    • Prop-drill sidebar/theme state through 5+ components
    • Use [locale] file-based routing for i18n
    • Create new Zustand stores per module
  • Do encapsulate all state, computed properties, and mutations inside dedicated ViewModels.

    • Use TanStack Query for all server-state (useQuery + useQueryClient)
    • Access Zustand directly where needed (useScripetore())
    • Use LanguageProvider + t() function with localStorage persistence
    • Use hooks + TanStack Query per module; Zustand only for global UI