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 Type | Tool | Location | Persistence |
|---|---|---|---|
| Server Data | TanStack Query v5 | ViewModels | In-memory cache (configurable staleTime) |
| Auth/Session | Zustand + persist | @core/store | localStorage |
| Theme/Sidebar | Zustand | @core/store | Optional |
| Toasts | Zustand | @core/store | None |
| Language | LanguageProvider | @core/providers | localStorage |
| Form Inputs | useState / React Hook Form | Components | None |
| Modal/Toggle | useState | Components | None |
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.
// 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 });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
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).
| Store | Purpose | Persisted? |
|---|---|---|
| useAppStore | User session, permissions, roles, sidebar status, density setting, tenant context | Yes (localStorage, partialized) |
| useCurrencyPreference | Preferred display currency and converting rules | Yes (only display mode setting) |
| useToastStore | Toast notification queue (auto-dismiss) | No |
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.
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