State Management

TanStack Query for server state, Zustand for global UI state, and LanguageProvider for localization.

SCRIPE uses three state management tools, each for a specific category: TanStack Query for server data (API results), Zustand for global UI state (auth, sidebar, theme), and useState for component-local state (forms, toggles).

Decision Matrix

QuestionAnswer: YES → UseExample
Does it come from an API?TanStack Query v5Employee list, audit logs, dashboard stats
Does the whole app need it?ZustandAuth state, sidebar, theme, toasts
Is it for this component only?useStateForm inputs, modal open/close, toggles
Does it need persistence?Zustand with persist middlewareAuth token, language preference
Does it need caching/refetch?TanStack QueryAny server data with stale-while-revalidate

TanStack Query (Server State)

Use TanStack Query for any data that comes from the API. It handles caching, background refetching, pagination, optimistic updates, and request deduplication automatically.

TanStack Query — ViewModel PatternTypeScript
// Key factory for consistent cache keys
/**
 * Exported constant defining parameters and fields for employee keys configurations.
 */
export const employeeKeys = {
  all: ["employees"] as const,
  list: (filters: { page: number; search: string }) =>
    [...employeeKeys.all, "list", filters] as const,
  detail: (id: string) =>
    [...employeeKeys.all, "detail", id] as const,
};

// Query hook
/**
 * React hook/ViewModel orchestrating state and data flows for employees.
 * Coordinates query synchronization (TanStack Query) with application client store indicators (Zustand) and returns validation fields.
 */
export function useEmployees(filters: { page: number; search: string }) {
  const repo = container.employeeRepository;

  return useQuery({
    queryKey: employeeKeys.list(filters),
    queryFn: async () => {
      const result = await repo.getAll(filters);
      if (result.isErr()) throw result.error;
      return result.value;
    },
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

// Mutation hook (auto-invalidates cache)
/**
 * React hook/ViewModel orchestrating state and data flows for create employee.
 * Coordinates query synchronization (TanStack Query) with application client store indicators (Zustand) and returns validation fields.
 */
export function useCreateEmployee() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (data: CreateEmployeeInput) => {
      const result = await container.employeeRepository.create(data);
      if (result.isErr()) throw result.error;
      return result.value;
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: employeeKeys.all });
    },
  });
}

Zustand (Global UI State)

Use Zustand for global UI state that needs to be shared across components but doesn't come from the server. There are exactly 3 approved stores.

StorePurposePersistenceLocation
useAuthStoreUser session, tokens, permissionslocalStorage (persist)@core/store/useAuthStore
useScripetoreSidebar, theme, mobile menuNone@core/store/useScripetore
useToastStoreToast notification queueNone@core/store/useToastStore
Auth Store — Zustand with persistTypeScript
export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      token: null,
      isAuthenticated: false,

      login: (user, token) => set({
        user, token, isAuthenticated: true,
      }),

      logout: () => set({
        user: null, token: null, isAuthenticated: false,
      }),
    }),
    {
      name: "auth-storage",
      partialize: (state) => ({
        token: state.token,
        user: state.user,
      }),
    }
  )
);

Anti-Patterns

  • DO

    • Use TanStack Query for ALL server data
    • Access Zustand stores directly where needed
    • Use useState for component-local state
    • Let TanStack Query handle caching & refetching
    • Use key factories for consistent cache keys
  • DON'T

    • DON'T put server data in Zustand
    • DON'T prop-drill global state through 5+ components
    • DON'T use useEffect+fetch for API calls
    • DON'T create manual cache invalidation
    • DON'T mix concerns in a single hook

Localization (LanguageProvider + Module Locales)

Localization uses a custom LanguageProvider with localStorage persistence plus a module-scoped locale system. Shared keys (~1,156) live in core/locales/. Module-specific keys are co-located in each module's locales/ directory and loaded lazily via useModuleLocales().

FeatureImplementationDetails
Language SwitchingLanguageProvider contextCookie/localStorage, NOT URL-based
RTL/LTR SupportAuto-set dir + lang on <html>Arabic: RTL, all others: LTR
Font Classesfont-arabic / font-english on <body>Auto-applied on switch
Dot-Notation Keyst('common.save')Nested dictionary access
Interpolationt('errors.minLength', { min: 5 }){{min}} placeholder replacement
SSR FallbackReturns key itself during SSRGraceful degradation