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

30 KiB
Raw Blame History

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