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
| Question | Answer: YES → Use | Example |
|---|---|---|
| Does it come from an API? | TanStack Query v5 | Employee list, audit logs, dashboard stats |
| Does the whole app need it? | Zustand | Auth state, sidebar, theme, toasts |
| Is it for this component only? | useState | Form inputs, modal open/close, toggles |
| Does it need persistence? | Zustand with persist middleware | Auth token, language preference |
| Does it need caching/refetch? | TanStack Query | Any 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.
// 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.
| Store | Purpose | Persistence | Location |
|---|---|---|---|
| useAuthStore | User session, tokens, permissions | localStorage (persist) | @core/store/useAuthStore |
| useScripetore | Sidebar, theme, mobile menu | None | @core/store/useScripetore |
| useToastStore | Toast notification queue | None | @core/store/useToastStore |
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().
| Feature | Implementation | Details |
|---|---|---|
| Language Switching | LanguageProvider context | Cookie/localStorage, NOT URL-based |
| RTL/LTR Support | Auto-set dir + lang on <html> | Arabic: RTL, all others: LTR |
| Font Classes | font-arabic / font-english on <body> | Auto-applied on switch |
| Dot-Notation Keys | t('common.save') | Nested dictionary access |
| Interpolation | t('errors.minLength', { min: 5 }) | {{min}} placeholder replacement |
| SSR Fallback | Returns key itself during SSR | Graceful degradation |