39948-vm/frontend/docs/stores-module.md
2026-07-03 16:11:24 +02:00

1045 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Frontend Stores Module
## Overview
The Stores module implements **Redux Toolkit** state management for the frontend application. It provides a centralized store with core UI/auth slices, entity CRUD slices, and runtime-setting slices for transition and global UI-control defaults.
**Location:** `frontend/src/stores/`
**Total Files:** 26+ TypeScript files
**Key Technologies:**
- Redux Toolkit 2.1.0
- Axios for API calls
- JWT for token handling
---
## Architecture
```
frontend/src/stores/
├── store.ts # Main Redux store configuration (~49 LOC)
├── hooks.ts # Typed hooks (useAppDispatch, useAppSelector) (~6 LOC)
├── createEntitySlice.ts # Entity slice factory (~337 LOC)
├── selectors.ts # Memoized selectors (~181 LOC)
├── Core Slices (4)
│ ├── authSlice.ts # Authentication state (~123 LOC)
│ ├── styleSlice.ts # UI styling/theming (~107 LOC)
│ ├── mainSlice.ts # Main app state (~32 LOC)
├── Entity/Runtime Setting Slices
│ ├── users/usersSlice.ts # (~25 LOC)
│ ├── roles/rolesSlice.ts # (~96 LOC)
│ ├── permissions/permissionsSlice.ts # (~25 LOC)
│ ├── projects/projectsSlice.ts # (~25 LOC)
│ ├── project_memberships/project_membershipsSlice.ts # (~25 LOC)
│ ├── assets/assetsSlice.ts # (~26 LOC)
│ ├── asset_variants/asset_variantsSlice.ts # (~25 LOC)
│ ├── presigned_url_requests/presigned_url_requestsSlice.ts # (~25 LOC)
│ ├── tour_pages/tour_pagesSlice.ts # (~25 LOC)
│ ├── project_audio_tracks/project_audio_tracksSlice.ts # (~25 LOC)
│ ├── publish_events/publish_eventsSlice.ts # (~25 LOC)
│ ├── pwa_caches/pwa_cachesSlice.ts # (~25 LOC)
│ ├── access_logs/access_logsSlice.ts # (~25 LOC)
│ ├── global_transition_defaults/globalTransitionDefaultsSlice.ts # Public GET (~138 LOC)
│ ├── project_transition_settings/projectTransitionSettingsSlice.ts # Public prod GET (~275 LOC)
│ ├── global_ui_control_defaults/globalUiControlDefaultsSlice.ts # Public GET singleton defaults
│ ├── project_ui_control_settings/projectUiControlSettingsSlice.ts # Project/env UI-control overrides
│ └── constructor/constructorSlice.ts # Constructor state (~110 LOC)
├── Data Files
│ └── introSteps.ts # Onboarding tour definitions (~122 LOC)
└── Legacy (deprecated)
└── usersSlice.ts # Old users slice (use users/usersSlice.ts)
```
---
## Store Configuration
**File:** `store.ts`
The main store combines all slice reducers:
```typescript
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {
// Core slices
style: styleReducer,
main: mainReducer,
auth: authSlice,
// Entity and runtime-setting slices
users: usersSlice,
roles: rolesSlice,
permissions: permissionsSlice,
projects: projectsSlice,
project_memberships: project_membershipsSlice,
assets: assetsSlice,
asset_variants: asset_variantsSlice,
presigned_url_requests: presigned_url_requestsSlice,
tour_pages: tour_pagesSlice,
project_audio_tracks: project_audio_tracksSlice,
publish_events: publish_eventsSlice,
pwa_caches: pwa_cachesSlice,
access_logs: access_logsSlice,
global_transition_defaults: globalTransitionDefaultsSlice,
project_transition_settings: projectTransitionSettingsSlice,
global_ui_control_defaults: globalUiControlDefaultsSlice,
project_ui_control_settings: projectUiControlSettingsSlice,
},
});
// Inferred types for type safety
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
```
---
## Typed Hooks
**File:** `hooks.ts`
```typescript
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// Use throughout app instead of plain useDispatch and useSelector
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
```
**Usage:**
```typescript
import { useAppDispatch, useAppSelector } from '../stores/hooks';
const MyComponent = () => {
const dispatch = useAppDispatch();
const users = useAppSelector((state) => state.users.users);
const loading = useAppSelector((state) => state.users.loading);
// Dispatch actions with full type inference
dispatch(fetch({ query: '?limit=100' }));
};
```
---
## Entity Slice Factory
**File:** `createEntitySlice.ts` (~342 LOC)
The factory generates Redux slices with standard CRUD operations for any entity.
### Configuration Interface
```typescript
interface EntitySliceConfig {
name: string; // Slice name (e.g., 'users')
endpoint: string; // API endpoint (e.g., 'users')
singularName?: string; // For notifications (e.g., 'User')
}
```
### Generated State
```typescript
interface EntitySliceState<T> {
[entityName]: T[]; // Entity data array
loading: boolean; // Loading indicator
count: number; // Total count for pagination
refetch: boolean; // Trigger refetch flag
rolesWidgets: unknown[];// Widgets (for roles slice)
notify: NotificationState;
}
interface NotificationState {
showNotification: boolean;
textNotification: string;
typeNotification: 'warn' | 'error' | 'success' | 'info' | '';
}
```
### Generated Async Thunks
| Thunk | Action Type | Purpose |
|-------|-------------|---------|
| `fetch` | `{name}/fetch` | Fetch single by ID or list with query |
| `create` | `{name}/create{Name}` | Create new entity |
| `update` | `{name}/update{Name}` | Update existing entity |
| `deleteItem` | `{name}/delete{Name}` | Delete single entity |
| `deleteItemsByIds` | `{name}/deleteByIds` | Bulk delete by IDs |
| `uploadCsv` | `{name}/uploadCsv` | Import CSV file |
### Generated Reducers
| Reducer | Purpose |
|---------|---------|
| `setRefetch` | Trigger data refetch |
### Usage Example
```typescript
// Creating a slice (entity file)
import { createEntitySlice } from '../createEntitySlice';
import type { User } from '../../types/entities';
const { slice, actions, reducer } = createEntitySlice<User>({
name: 'users',
endpoint: 'users',
singularName: 'User',
});
export const {
fetch,
create,
update,
deleteItem,
deleteItemsByIds,
uploadCsv,
setRefetch,
} = actions;
export default reducer;
```
```typescript
// Using in components
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { fetch, create, update, deleteItem } from '../stores/users/usersSlice';
const UsersPage = () => {
const dispatch = useAppDispatch();
const { users, loading, count, notify } = useAppSelector((state) => state.users);
// Fetch list with pagination
useEffect(() => {
dispatch(fetch({ query: '?page=1&limit=10' }));
}, []);
// Fetch single by ID
const loadUser = (id: string) => {
dispatch(fetch({ id }));
};
// Create new
const createUser = (data: Partial<User>) => {
dispatch(create(data));
};
// Update existing
const updateUser = (id: string, data: Partial<User>) => {
dispatch(update({ id, data }));
};
// Delete
const removeUser = (id: string) => {
dispatch(deleteItem(id));
};
};
```
### Factory Implementation Details
```typescript
// Fetch thunk handles both single and list
const fetch = createAsyncThunk<
T | PaginatedResponse<T>,
FetchParams,
{ rejectValue: ApiError }
>(`${name}/fetch`, async (data: FetchParams) => {
const { id, query } = data;
const url = `${endpoint}${query || (id ? `/${id}` : '')}`;
const result = await axios.get<T | PaginatedResponse<T>>(url);
if (id) {
return result.data as T; // Single entity
}
return {
rows: (result.data as PaginatedResponse<T>).rows,
count: (result.data as PaginatedResponse<T>).count,
};
});
// Extrareducers handle state updates
builder.addCase(fetch.fulfilled, (state, action) => {
const payload = action.payload;
if (isPaginatedResponse<T>(payload)) {
state[name] = payload.rows;
state.count = payload.count;
} else {
state[name] = payload; // Single entity stored in array
}
state.loading = false;
});
```
---
## Core Slices
### 1. authSlice
**File:** `authSlice.ts` (~130 LOC)
**Purpose:** Authentication state management.
```typescript
interface AuthState {
isFetching: boolean;
errorMessage: string;
currentUser: User | null;
token: string;
notify: NotificationState;
}
```
**Async Thunks:**
| Thunk | Endpoint | Purpose |
|-------|----------|---------|
| `loginUser` | `POST /auth/signin/local` | Local authentication |
| `findMe` | `GET /auth/me` | Fetch current user |
| `passwordReset` | `PUT /auth/password-reset` | Reset password |
**Reducers:**
| Reducer | Purpose |
|---------|---------|
| `logoutUser` | Clear auth state, remove tokens |
**Token Handling:**
```typescript
builder.addCase(loginUser.fulfilled, (state, action) => {
const token = action.payload;
const user = jwt.decode(token);
state.token = token;
sessionStorage.setItem('token', token);
localStorage.setItem('token', token);
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
});
```
**Usage:**
```typescript
import { loginUser, logoutUser, findMe } from '../stores/authSlice';
// Login
dispatch(loginUser({ email, password }));
// Logout
dispatch(logoutUser());
// Fetch current user
dispatch(findMe());
// Access state
const { currentUser, isFetching, errorMessage } = useAppSelector(state => state.auth);
```
---
### 2. styleSlice
**File:** `styleSlice.ts` (~107 LOC)
**Purpose:** UI theming and styling state.
```typescript
interface StyleState {
darkMode: boolean;
bgLayoutColor: string;
iconsColor: string;
activeLinkColor: string;
cardsColor: string;
focusRingColor: string;
corners: string;
cardsStyle: string;
linkColor: string;
borders: string;
shadow: string;
textSecondary: string;
// Aside menu styles
asideStyle: string;
asideScrollbarsStyle: string;
asideBrandStyle: string;
asideMenuItemStyle: string;
asideMenuItemActiveStyle: string;
asideMenuDropdownStyle: string;
// NavBar styles
navBarItemLabelStyle: string;
navBarItemLabelHoverStyle: string;
navBarItemLabelActiveColorStyle: string;
overlayStyle: string;
websiteHeder: string;
websiteSectionStyle: string;
}
```
**Reducers:**
| Reducer | Purpose |
|---------|---------|
| `setDarkMode` | Toggle dark/light mode |
| `setStyle` | Apply complete style theme |
**Dark Mode Handling:**
```typescript
setDarkMode: (state, action: PayloadAction<boolean | null>) => {
state.darkMode = action.payload !== null ? action.payload : !state.darkMode;
localStorage.setItem(localStorageDarkModeKey, state.darkMode ? '1' : '0');
document.body.classList[state.darkMode ? 'add' : 'remove']('dark-scrollbars');
document.documentElement.classList[state.darkMode ? 'add' : 'remove']('dark-scrollbars-compat');
}
```
**Usage:**
```typescript
import { setDarkMode, setStyle } from '../stores/styleSlice';
// Toggle dark mode
dispatch(setDarkMode(null)); // Toggle
dispatch(setDarkMode(true)); // Force dark
dispatch(setDarkMode(false)); // Force light
// Apply style theme
dispatch(setStyle('white'));
dispatch(setStyle('basic'));
// Access state
const { darkMode, corners, cardsStyle, iconsColor } = useAppSelector(state => state.style);
```
---
### 3. mainSlice
**File:** `mainSlice.ts` (~36 LOC)
**Purpose:** Main application UI state.
```typescript
interface MainState {
userName: string;
userEmail: string | null;
userAvatar: string | null;
isFieldFocusRegistered: boolean;
}
```
**Reducers:**
| Reducer | Purpose |
|---------|---------|
| `setUser` | Update user display info |
**Usage:**
```typescript
import { setUser } from '../stores/mainSlice';
dispatch(setUser({
name: 'John Doe',
email: 'john@example.com',
avatar: '/avatars/john.jpg',
}));
const { userName, userEmail, userAvatar } = useAppSelector(state => state.main);
```
---
## Entity Slices
All 13 entity slices follow the factory pattern:
### Standard Entity Slice Pattern
```typescript
/**
* {Entity} Redux Slice
*/
import { createEntitySlice } from '../createEntitySlice';
import type { EntityType } from '../../types/entities';
const { slice, actions, reducer } = createEntitySlice<EntityType>({
name: 'entity_name',
endpoint: 'api_endpoint',
singularName: 'Entity Name',
});
export const {
fetch,
create,
update,
deleteItem,
deleteItemsByIds,
uploadCsv,
setRefetch,
} = actions;
export const entitySlice = slice;
export default reducer;
```
### Entity Slice Inventory
| Slice | Entity Type | Endpoint | Singular Name |
|-------|-------------|----------|---------------|
| `users` | `User` | `users` | User |
| `roles` | `Role` | `roles` | Role |
| `permissions` | `PermissionEntity` | `permissions` | Permission |
| `projects` | `Project` | `projects` | Project |
| `project_memberships` | `ProjectMembership` | `project_memberships` | Project Membership |
| `assets` | `Asset` | `assets` | Asset |
| `asset_variants` | `AssetVariant` | `asset_variants` | Asset Variant |
| `presigned_url_requests` | `PresignedUrlRequest` | `presigned_url_requests` | Presigned URL Request |
| `tour_pages` | `TourPage` | `tour_pages` | Tour Page |
| `project_audio_tracks` | `ProjectAudioTrack` | `project_audio_tracks` | Project Audio Track |
| `publish_events` | `PublishEvent` | `publish_events` | Publish Event |
| `pwa_caches` | `PwaCache` | `pwa_caches` | PWA Cache |
| `access_logs` | `AccessLog` | `access_logs` | Access Log |
| `global_transition_defaults` | `GlobalTransitionDefaults` | `global-transition-defaults` | Global defaults (singleton, public GET) |
| `project_transition_settings` | `ProjectTransitionSettingsEntity` | `project-transition-settings` | Per-project settings (public prod GET) |
## Type Definitions
### Redux Types (`types/redux.ts`)
```typescript
// Notification state structure
export interface NotificationState {
showNotification: boolean;
textNotification: string;
typeNotification: 'warn' | 'error' | 'success' | 'info' | '';
}
// Generic entity slice state
export interface EntitySliceState<T> {
data: T[];
loading: boolean;
count: number;
refetch: boolean;
notify: NotificationState;
}
// Slice factory configuration
export interface EntitySliceConfig {
name: string;
endpoint: string;
singularName?: string;
}
// Auth state structure
export interface AuthState {
currentUser: User | null;
token: string | null;
isFetching: boolean;
errorMessage: string | null;
notify: NotificationState;
}
```
### API Types (`types/api.ts`)
```typescript
// Paginated response from API
export interface PaginatedResponse<T> {
rows: T[];
count: number;
}
// Fetch parameters for entity queries
export interface FetchParams {
id?: string;
query?: string;
limit?: number;
page?: number;
}
// API error response structure
export interface ApiError {
message?: string;
statusCode?: number;
errors?: Record<string, { _errors: string[] }>;
}
// Sort model for data grid
export interface SortModel {
field: string;
sort: 'asc' | 'desc';
}
// Query parameters for list requests
export interface ListQueryParams {
page?: number;
limit?: number;
sort?: 'asc' | 'desc';
field?: string;
[filterKey: string]: string | number | boolean | undefined;
}
```
### Entity Types (`types/entities.ts`)
```typescript
// Base entity interface
export interface BaseEntity {
id: string;
createdAt?: string;
updatedAt?: string;
}
// User entity
export interface User extends BaseEntity {
firstName?: string;
lastName?: string;
email: string;
disabled?: boolean;
avatar?: ImageFile[];
app_role?: Role | null;
custom_permissions?: PermissionEntity[];
}
// Project entity
export interface Project extends BaseEntity {
name: string;
slug?: string;
description?: string;
logo_url?: string;
favicon_url?: string;
og_image_url?: string;
}
// Asset entity
export interface Asset extends BaseEntity {
project?: Project | string | null;
name: string;
asset_type: 'image' | 'video' | 'audio' | 'file';
type?: 'general' | 'icon' | 'background_image' | 'audio' | 'video' | 'transition' | 'logo' | 'favicon' | 'document';
cdn_url?: string;
storage_key?: string;
mime_type?: string;
size_mb?: number;
width_px?: number;
height_px?: number;
duration_sec?: number;
is_public?: boolean;
is_deleted?: boolean;
}
// ... (13 total entity types)
```
---
## Notification Helpers
**File:** `helpers/notifyStateHandler.ts`
Utility functions for managing notification state:
```typescript
// Reset notification state
export const resetNotify = (state) => {
state.notify.showNotification = false;
state.notify.typeNotification = '';
state.notify.textNotification = '';
};
// Handle rejected action with error notification
export const rejectNotify = (state, action) => {
if (typeof action.payload === 'string') {
state.notify.textNotification = action.payload;
} else if (typeof action === 'object') {
// Parse validation errors
const obj = { ...action.payload?.errors };
delete obj['_errors'];
let msg = '';
for (const key in obj) {
msg += `${key}: ${obj[key]['_errors']}; \n `;
}
state.notify.textNotification = msg;
} else {
state.notify.textNotification = 'Network error';
}
state.notify.typeNotification = 'error';
state.notify.showNotification = true;
};
// Handle fulfilled action with success notification
export const fulfilledNotify = (state, msg) => {
state.notify.textNotification = msg;
state.notify.typeNotification = 'success';
state.notify.showNotification = true;
};
```
---
## Onboarding Tour Steps
**File:** `introSteps.ts` (~135 LOC)
Defines step configurations for the app onboarding tour:
```typescript
interface Step {
element: string; // CSS selector
intro: string; // HTML content
position?: string; // Tooltip position
tooltipClass?: string; // Custom CSS class
disableInteraction?: boolean;
}
// Exported step arrays
export const loginSteps: Step[] = [...]; // Login page tour
export const appSteps: Step[] = [...]; // Main app tour
export const usersSteps: Step[] = [...]; // Users page tour
export const rolesSteps: Step[] = [...]; // Roles page tour
```
---
## Data Flow Diagram
```
┌─────────────────────────────────────────────────────────────────┐
│ Component │
│ useAppDispatch() + useAppSelector() │
└─────────────────────┬───────────────────────────────────────────┘
│ dispatch(fetch({ query }))
┌─────────────────────────────────────────────────────────────────┐
│ Async Thunk │
│ fetch, create, update, deleteItem, etc. │
└─────────────────────┬───────────────────────────────────────────┘
│ axios.get/post/put/delete
┌─────────────────────────────────────────────────────────────────┐
│ Backend API │
│ /api/{endpoint} │
└─────────────────────┬───────────────────────────────────────────┘
│ Response
┌─────────────────────────────────────────────────────────────────┐
│ Extra Reducers │
│ pending → loading: true │
│ fulfilled → data: payload, loading: false │
│ rejected → notify: error, loading: false │
└─────────────────────┬───────────────────────────────────────────┘
│ State update
┌─────────────────────────────────────────────────────────────────┐
│ Store │
│ RootState with 17 slices │
└─────────────────────┬───────────────────────────────────────────┘
│ useAppSelector subscription
┌─────────────────────────────────────────────────────────────────┐
│ Component Re-render │
│ UI updates with new data │
└─────────────────────────────────────────────────────────────────┘
```
---
## Usage Patterns
### 1. List Page Pattern
```typescript
import { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { fetch, setRefetch } from '../stores/users/usersSlice';
const UsersListPage = () => {
const dispatch = useAppDispatch();
const { users, loading, count, refetch } = useAppSelector(state => state.users);
const [page, setPage] = useState(1);
// Initial fetch
useEffect(() => {
dispatch(fetch({ query: `?page=${page}&limit=10` }));
}, [page, dispatch]);
// Refetch trigger
useEffect(() => {
if (refetch) {
dispatch(fetch({ query: `?page=${page}&limit=10` }));
dispatch(setRefetch(false));
}
}, [refetch, page, dispatch]);
return (
<DataGrid
rows={users}
loading={loading}
rowCount={count}
onPageChange={setPage}
/>
);
};
```
### 2. Edit Page Pattern
```typescript
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { fetch, update } from '../stores/users/usersSlice';
const UserEditPage = () => {
const router = useRouter();
const { id } = router.query;
const dispatch = useAppDispatch();
const { users, loading, notify } = useAppSelector(state => state.users);
// Fetch entity on mount
useEffect(() => {
if (id) {
dispatch(fetch({ id: id as string }));
}
}, [id, dispatch]);
// Get current entity
const user = Array.isArray(users) ? users[0] : users;
const handleSubmit = async (data: Partial<User>) => {
await dispatch(update({ id: id as string, data }));
router.push('/users/users-list');
};
return (
<Formik initialValues={user} onSubmit={handleSubmit}>
{/* Form fields */}
</Formik>
);
};
```
### 3. Create Page Pattern
```typescript
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { create } from '../stores/users/usersSlice';
const UserNewPage = () => {
const dispatch = useAppDispatch();
const router = useRouter();
const { loading, notify } = useAppSelector(state => state.users);
const handleSubmit = async (data: Partial<User>) => {
const result = await dispatch(create(data));
if (result.meta.requestStatus === 'fulfilled') {
router.push('/users/users-list');
}
};
return (
<Formik initialValues={{}} onSubmit={handleSubmit}>
{/* Form fields */}
</Formik>
);
};
```
### 4. Delete Pattern
```typescript
import { deleteItem, deleteItemsByIds, setRefetch } from '../stores/users/usersSlice';
// Delete single
const handleDelete = async (id: string) => {
await dispatch(deleteItem(id));
dispatch(setRefetch(true));
};
// Bulk delete
const handleBulkDelete = async (ids: string[]) => {
await dispatch(deleteItemsByIds(ids));
dispatch(setRefetch(true));
};
```
### 5. Notification Handling
```typescript
import { useEffect } from 'react';
import { toast } from 'react-toastify';
const MyComponent = () => {
const { notify } = useAppSelector(state => state.users);
useEffect(() => {
if (notify.showNotification) {
if (notify.typeNotification === 'success') {
toast.success(notify.textNotification);
} else if (notify.typeNotification === 'error') {
toast.error(notify.textNotification);
}
}
}, [notify]);
};
```
---
## Store Provider Setup
**File:** `_app.tsx`
```typescript
import { Provider } from 'react-redux';
import { store } from '../stores/store';
function MyApp({ Component, pageProps }) {
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
export default MyApp;
```
---
## State Management Guidelines
### When to Use Redux (Default)
Redux is the **default choice** for application state. Use Redux slices for:
| State Type | Example | Why Redux |
|------------|---------|-----------|
| **Entity Data** | Users, Projects, Assets, Tour Pages | Shared across multiple components, API-backed |
| **Authentication** | Current user, JWT token | App-wide, persisted to localStorage |
| **UI Preferences** | Dark mode, theme settings | Persisted, affects entire app |
| **Form State** | Complex multi-step forms | Survives navigation, shareable |
| **Notifications** | Toast messages | Triggered from anywhere |
### When Local Hooks Are Acceptable
Local React hooks (`useState`, custom hooks) are appropriate for:
| State Type | Example | Why Local |
|------------|---------|-----------|
| **Ephemeral UI State** | Modal open/closed, dropdown expanded | Component-scoped, no persistence needed |
| **Session-Scoped Navigation** | Page history in `usePageNavigation` | Isolated to single route, resets on unmount |
| **Animation State** | Transition progress, fade states | Highly transient, no sharing needed |
| **Form Field Focus** | Which input is focused | Local to form component |
| **Derived/Computed Values** | Filtered lists, sorted data | Computed from Redux state |
### Decision Tree
```
Is the state needed by multiple unrelated components?
├── Yes → Use Redux
└── No → Does the state need to persist across route changes?
├── Yes → Use Redux
└── No → Is the state tied to API data?
├── Yes → Use Redux
└── No → Local hook is acceptable
```
### Example: usePageNavigation (Local Hook)
The `usePageNavigation` hook uses local `useState` because:
- ✅ Only used by RuntimePresentation OR constructor (never both simultaneously)
- ✅ Session-scoped - history resets when leaving the tour
- ✅ No cross-component sharing needed
- ✅ No persistence requirement
- ✅ Simple data structure (array + string)
```typescript
// Appropriate for local hook
const { currentPageId, pageHistory, applyPageSelection } = usePageNavigation({
pages,
trackHistory: true,
});
```
### Example: Constructor Elements (Redux)
The `constructorSlice` uses Redux because:
- ✅ Multiple components need access (canvas, sidebar, settings panels)
- ✅ Complex nested state (elements, selections, undo history)
- ✅ DevTools debugging valuable
- ✅ Actions dispatched from many places
```typescript
// Appropriate for Redux
const elements = useAppSelector((state) => state.constructor.elements);
dispatch(updateElement({ id, changes }));
```
### Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
|--------------|---------|----------|
| Redux for modal open/close | Over-engineering | Local `useState` |
| Local state for entity data | Data inconsistency | Redux slice |
| Prop drilling 3+ levels | Maintenance burden | Redux or Context |
| Redux for animation frames | Performance issues | Local `useRef` |
---
## Summary
| Category | Count | Description |
|----------|-------|-------------|
| Core Slices | 4 | auth, style, main, openAi |
| Entity Slices | 15 | Generated via factory pattern (includes 2 transition slices) |
| Constructor Slice | 1 | Tour editor state |
| Type Files | 3 | redux.ts, entities.ts, api.ts |
| Total Slices | 20 | Combined in store.ts |
### Public Access Slices
Some slices support public API access for runtime presentations:
| Slice | Public Access | Notes |
|-------|--------------|-------|
| `global_transition_defaults` | GET always public | No auth headers needed |
| `project_transition_settings` | GET production only | Backend determines from URL path |
These slices don't require special headers - the backend determines public access from the URL path itself.
| Total Files | 22+ | In stores directory |
**Key Patterns:**
- **Redux is default** for app-wide, persistent, shared state
- **Local hooks acceptable** for ephemeral, component-scoped state
- Factory pattern eliminates ~2,600 LOC of boilerplate (13 entities × ~200 LOC each)
- Typed hooks ensure type safety throughout
- Consistent notification handling via helpers
- Async thunks with proper error handling
---
## Related Documentation
- [Frontend Architecture](./frontend-architecture.md) - Overall frontend structure
- [Hooks Module](./hooks-module.md) - Custom React hooks
- [Components Module](./components-module.md) - React components
- [Backend API Endpoints](../../backend/docs/api-endpoints.md) - API reference