30 KiB
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:
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
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:
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
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
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
// 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;
// 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
// 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.
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:
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:
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.
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:
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:
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.
interface MainState {
userName: string;
userEmail: string | null;
userAvatar: string | null;
isFieldFocusRegistered: boolean;
}
Reducers:
| Reducer | Purpose |
|---|---|
setUser |
Update user display info |
Usage:
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
/**
* {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)
// 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)
// 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)
// 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:
// 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:
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
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
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
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
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
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
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)
// 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
// 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 - Overall frontend structure
- Hooks Module - Custom React hooks
- Components Module - React components
- Backend API Endpoints - API reference