# Frontend Hooks Module ## Overview The Hooks module provides **47 custom React hooks** for reusable logic across the frontend application. Hooks handle everything from runtime presentation (preloading, navigation, transitions) to admin pages (tables, forms, filters), the constructor (elements, drag-drop), and React Query data fetching. **Location:** `frontend/src/hooks/` **Total Files:** 56 TypeScript files (41 hooks + 13 query hooks + 2 index files) **Recent Refactors:** 1. **Page Navigation Consolidation:** 6+ fragmented hooks consolidated into `usePageNavigationState` state machine. See [Navigation State Machine](#usepagenavigationstate) for details. 2. **Video Hooks Module:** New `hooks/video/` directory with 8 primitive hooks for composable video playback. See [Video Hooks Module](./video-hooks-module.md) for details. 3. **Preloading Simplification:** Removed neighbor preloading (stream-first approach). Constructor always uses online mode - transition videos stream on-demand, then cache for replay. --- ## State Management Context **Redux is the default** for app-wide state management. These hooks complement Redux by handling: - **Ephemeral state** - Transient UI states that reset on unmount - **Computed/derived values** - Data derived from Redux state - **DOM interactions** - Refs, observers, event handlers - **Session-scoped state** - State that doesn't persist across routes See [Stores Module - State Management Guidelines](./stores-module.md#state-management-guidelines) for the decision tree on when to use Redux vs local hooks. **Example:** `usePageNavigation` uses local `useState` because page history is session-scoped and isolated to a single route. If cross-component access or persistence were needed, it would be a Redux slice. --- ## Architecture ``` frontend/src/hooks/ ├── index.ts # Central exports (tree-shaking friendly, 67 LOC) │ ├── Runtime/Preloading Hooks (10) │ ├── usePreloadOrchestrator.ts # Main asset preloading coordinator (~720 LOC) [SIMPLIFIED] │ ├── usePageNavigationState.ts # Unified page navigation state machine (~520 LOC) │ ├── useTransitionPlayback.ts # Video transition playback (~795 LOC) │ ├── useSlideTransition.ts # Slide transition animation (~180 LOC) │ ├── usePageNavigation.ts # Page navigation state + history (~195 LOC) │ ├── useBackgroundVideoPlayback.ts # Background video time control + play once (~225 LOC) │ ├── useVideoSoundControl.ts # iOS autoplay sound control (~100 LOC) │ ├── usePageDataLoader.ts # Project/page data loading (~253 LOC) │ ├── usePageBackground.ts # Page background management (~176 LOC) │ └── useCanvasScale.ts # Responsive canvas scaling (~110 LOC) │ │ # DELETED (consolidated into usePageNavigationState): │ # - usePageSwitch.ts (~475 LOC) │ # - useBackgroundTransition.ts (~164 LOC) │ # - useBackgroundUrls.ts (~104 LOC) │ │ # DELETED (simplified preloading - no neighbor traversal): │ # - useNeighborGraph.ts (~282 LOC) │ ├── Video Hooks (8) │ ├── video/index.ts # Exports (~60 LOC) │ ├── video/useVideoEventManager.ts # Event listener management (~90 LOC) │ ├── video/useVideoBufferingState.ts # Buffering detection (~180 LOC) │ ├── video/useVideoBlobUrl.ts # Blob URL resolution (~150 LOC) │ ├── video/useVideoFirstFrame.ts # First frame detection (~130 LOC) │ ├── video/useVideoErrorRecovery.ts # Error handling + retry (~160 LOC) │ ├── video/useVideoTimeouts.ts # Timeout management (~80 LOC) │ ├── video/useVideoPlaybackCore.ts # Composite playback logic (~280 LOC) │ └── video/useVideoPlayer.ts # UI video player (~200 LOC) │ ├── Audio Hooks (2) - NEW │ ├── audio/useAudioEventManager.ts # Audio event listener management (~115 LOC) │ └── useBackgroundAudioPlayback.ts # Background audio playback + ducking (~220 LOC) │ ├── PWA/Offline Hooks (5) │ ├── useOfflineMode.ts # PWA offline management (~468 LOC) │ ├── usePWAPreload.ts # PWA asset preloading (~199 LOC) │ ├── useStorageQuota.ts # Storage quota monitoring (~110 LOC) │ ├── useNetworkAware.ts # Network condition monitoring (~163 LOC) │ └── usePreloadProgress.ts # Preload progress tracking (~220 LOC) │ ├── Constructor Hooks (10) │ ├── useConstructorElements.ts # Canvas element CRUD + local clipboard │ ├── useCanvasElementDrag.ts # Element drag handling (~150 LOC) │ ├── useConstructorPageActions.ts # Page save/create/duplicate support │ ├── useConstructorData.ts # Constructor data loading (~150 LOC) │ ├── useTransitionPreview.ts # Transition preview state (~184 LOC) │ ├── useTransitionCreation.ts # Transition creation state (~134 LOC) │ ├── useCanvasElapsedTime.ts # Canvas animation timing (~111 LOC) │ ├── useMediaDurationProbe.ts # Media duration detection (~219 LOC) │ └── useBackdropEffect.ts # Backdrop visual effects (~190 LOC) │ ├── Table/List Hooks (3) │ ├── useEntityTable.ts # Complete table state management (~288 LOC) │ ├── useFilterItems.ts # Filter state for data tables (~166 LOC) │ └── useCSVHandling.ts # CSV upload/download operations (~141 LOC) │ ├── Form Hooks (2) │ ├── useEditPageSync.ts # Edit page form sync (~166 LOC) │ └── useFormSync.ts # Form state management (~108 LOC) │ ├── UI Utility Hooks (10) │ ├── useDraggable.ts # Draggable panel management (~200 LOC) │ ├── useOutsideClick.ts # Click outside detection (~88 LOC) │ ├── useElementEffects.ts # Element interactive effects (~128 LOC) │ ├── useIconPreload.ts # Icon preloading (~176 LOC) │ ├── useDashboardCounts.ts # Dashboard entity counts (~308 LOC) │ ├── useDevCompilationStatus.ts # Dev compilation status (~44 LOC) │ ├── useProjectAssets.ts # Project asset URL resolution (~102 LOC) │ ├── useAssetOptions.ts # Asset select options (~119 LOC) │ ├── usePublishStatus.ts # Publish/save timestamp status (~113 LOC) │ └── useAudioEffects.ts # Audio playback on hover/click (~305 LOC) │ ├── queries/ # React Query data fetching hooks (13) │ ├── index.ts # Query exports (~59 LOC) │ ├── useAccessLogsQuery.ts # Access logs query (~51 LOC) │ ├── useAssetVariantsQuery.ts # Asset variants query (~43 LOC) │ ├── useAssetsQuery.ts # Assets query (~109 LOC) │ ├── useElementDefaultsQuery.ts # Element defaults query (~50 LOC) │ ├── usePagesQuery.ts # Tour pages query (~115 LOC) │ ├── usePermissionsQuery.ts # Permissions query (~35 LOC) │ ├── useProjectAudioTracksQuery.ts # Audio tracks query (~109 LOC) │ ├── useProjectMembershipsQuery.ts # Project memberships query (~94 LOC) │ ├── useProjectQuery.ts # Project query (~84 LOC) │ ├── usePublishEventsQuery.ts # Publish events query (~56 LOC) │ ├── usePwaCachesQuery.ts # PWA caches query (~67 LOC) │ ├── useRolesQuery.ts # Roles query (~112 LOC) │ └── useUsersQuery.ts # Users query (~126 LOC) │ └── Exports └── index.ts # Tree-shaking friendly exports ``` --- ## Hook Categories ### 1. Runtime/Preloading Hooks These hooks power the runtime presentation viewer, handling asset preloading, page switching, and video transitions. #### usePreloadOrchestrator **File:** `usePreloadOrchestrator.ts` (~720 LOC) **Purpose:** Main coordinator for asset preloading with priority queues and blob URL caching. Uses a **stream-first** approach - only preloads current page assets and outgoing transition videos. ```typescript interface UsePreloadOrchestratorOptions { pages: PreloadPage[]; pageLinks: PreloadPageLink[]; elements: PreloadElement[]; currentPageId: string | null; pageHistory?: string[]; // Used for detecting back navigation enabled?: boolean; } interface UsePreloadOrchestratorResult { isPreloading: boolean; currentPhase: 'idle' | 'phase1_current_page' | 'phase2_transitions'; preloadedUrls: Set; queueLength: number; /** Version counter that increments when blob URLs become ready (triggers re-renders) */ readyUrlsVersion: number; preloadAsset: (url: string, priority?: number) => void; clearQueue: () => void; getCachedBlobUrl: (url: string) => Promise; isUrlPreloaded: (url: string) => Promise; getReadyBlobUrl: (url: string) => string | null; } ``` **Key Features:** - **Stream-first approach:** Only preloads current page + outgoing transitions (no neighbor preloading) - Priority-based preload queue (transition videos +150, images +100) - Two phases: Phase 1 (current page backgrounds) → Phase 2 (transition videos) - Dual storage strategy: Cache API (< 5MB) vs IndexedDB (>= 5MB) - Presigned URL management with proxy fallback - Image pre-decoding for instant display - O(1) blob URL lookup via `readyBlobUrlsRef` **Recent Simplification:** The hook was refactored to remove neighbor graph traversal (previously used `useNeighborGraph`). This simplifies the system: - Constructor always uses online mode - Transition videos stream on-demand, then cache for replay - Eliminates network contention from aggressive preloading **Usage:** ```typescript const preloadOrchestrator = usePreloadOrchestrator({ pages, pageLinks, elements, currentPageId, pageHistory, enabled: true, }); // Get cached blob URL for instant display const blobUrl = preloadOrchestrator.getReadyBlobUrl(originalUrl); // Manually preload with high priority preloadOrchestrator.preloadAsset(transitionVideoUrl, 150); ``` --- #### usePageNavigationState **File:** `usePageNavigationState.ts` (~520 LOC) **Purpose:** Unified state machine for page navigation using `useReducer` for atomic state transitions. Consolidates 6+ fragmented hooks to prevent race conditions. **Consolidates:** - `usePageSwitch` - URL resolution and switching - `useBackgroundState` - Background ready tracking - `useBackgroundTransition` - Fade-from-black effects - `useTransitionCleanup` - Video cleanup coordination - `useBackgroundUrls` - URL resolution for display - `pageLoadingUtils` - Loading state computation **State Machine Phases:** ``` ┌──────────────────────────────────────────────────────────────────┐ │ PAGE NAVIGATION STATE MACHINE │ ├──────────────────────────────────────────────────────────────────┤ │ ┌───────┐ navigate() ┌───────────┐ │ │ │ IDLE │ ─────────────► │ PREPARING │ │ │ └───────┘ └─────┬─────┘ │ │ ▲ YES / \ NO (has transition?) │ │ │ ▼ ▼ │ │ │ ┌─────────────┐ ┌────────────┐ │ │ │ │TRANSITIONING│ │ LOADING_BG │ │ │ │ └──────┬──────┘ └─────┬──────┘ │ │ │ video ends onBackgroundReady │ │ │ ▼ │ │ │ │ ┌─────────────┐ │ │ │ │ │TRANS_DONE │ │ │ │ │ └──────┬──────┘ │ │ │ │ onBackgroundReady │ │ │ │ ▼ ▼ │ │ │ ┌────────────────────────┐ │ │ │ │ FADING_IN │ │ │ │ └───────────┬────────────┘ │ │ │ fade complete │ │ └────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘ ``` ```typescript type NavigationPhase = | 'idle' // No navigation in progress | 'preparing' // Resolving URLs, saving previous state | 'transitioning' // Video transition playing | 'transition_done'// Video finished, waiting for background | 'loading_bg' // Direct navigation, waiting for background | 'fading_in'; // Black overlay fading out interface UsePageNavigationStateResult { // Current state phase: NavigationPhase; // Current page URLs (for display) currentImageUrl: string; currentVideoUrl: string; currentAudioUrl: string; // Previous page URLs (for overlay) previousImageUrl: string; previousVideoUrl: string; // Derived states (computed from phase) isLoading: boolean; showSpinner: boolean; showElements: boolean; showPreviousOverlay: boolean; showTransitionVideo: boolean; isFadingIn: boolean; isSwitching: boolean; isNewBgReady: boolean; isBackgroundReady: boolean; pendingTransitionComplete: boolean; // Transition style for CSS transitionStyle: React.CSSProperties; // Actions navigateToPage: (targetPage, options?) => Promise; onBackgroundReady: () => void; onTransitionEnded: () => void; setBackgroundDirectly: (imageUrl, videoUrl, audioUrl) => void; onVideoBufferStateChange: (isBuffering: boolean) => void; } ``` **Key Benefits:** | Before | After | |--------|-------| | 6+ hooks with 15+ state variables | 1 hook with 1 state object | | Race conditions from async setState | Atomic transitions via useReducer | | Invalid flag combinations possible | State machine prevents invalid states | | Derived state scattered | All derived state in one useMemo | | Hard to debug | Single phase value to inspect | **Usage:** ```typescript const navState = usePageNavigationState({ preloadCache: preloadOrchestrator ? { getReadyBlobUrl: preloadOrchestrator.getReadyBlobUrl, getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl, preloadedUrls: preloadOrchestrator.preloadedUrls, } : undefined, transitionSettings, }); // Navigate to a page await navState.navigateToPage(targetPage, { hasTransition: false, isBack: false, onSwitched: () => applyPageSelection(targetPage.id, false), }); // Use in CanvasBackground ``` **Context Provider:** A `PageNavigationContext` is available for components that need access to navigation state: ```typescript import { PageNavigationProvider, usePageNavigationContext } from '../context/PageNavigationContext'; // In parent component // In child component const { phase, onBackgroundReady } = usePageNavigationContext(); ``` --- #### useTransitionPlayback **File:** `useTransitionPlayback.ts` (~795 LOC) **Purpose:** Coordinates video transition playback with forward and pre-generated reversed video support. ```typescript interface TransitionPlaybackOptions { transition: TransitionPreviewState | null; pendingPageId: string; transitionVideoRef: React.RefObject; preloadOrchestrator?: UsePreloadOrchestratorResult; onTransitionComplete: (targetPageId: string) => void; onTransitionError?: (error: string) => void; } interface TransitionPlaybackResult { isPlaying: boolean; play: () => Promise; stop: () => void; pendingTransitionComplete: boolean; } ``` **Reverse Modes:** | Mode | Description | |------|-------------| | `none` | Forward playback only | | `separate` | Use pre-generated reversed video (server-side FFmpeg reversal) | Reversed videos are pre-generated on the server when pages are saved, using FFmpeg for audio/video synchronization. **Source URL Selection:** ```typescript const sourceUrl = useMemo(() => { if (!transition) return ''; // Use reversed video if back navigation with separate reversed video if (transition.isBack && transition.reverseVideoUrl) { return transition.reverseVideoUrl; } return transition.videoUrl; }, [transition]); ``` **Usage:** ```typescript const transitionPlayback = useTransitionPlayback({ transition: transitionPreview, pendingPageId, transitionVideoRef, preloadOrchestrator, onTransitionComplete: (targetId) => { pageSwitch.switchToPage(pages.find(p => p.id === targetId)); transitionPreviewHook.closePreview(); }, }); ``` --- #### useSlideTransition **File:** `useSlideTransition.ts` (~180 LOC) **Purpose:** Manages slide transition animation state for Gallery and Carousel elements. Implements fade-through-overlay animation pattern. **Animation Phases:** ``` Phase 1: Fade Out (half duration) ├── overlayOpacity: 0 → 1 └── slideOpacity: 1 → 0 Phase 2: Swap (at midpoint) └── displayIndex switches to new slide Phase 3: Fade In (half duration) ├── overlayOpacity: 1 → 0 └── slideOpacity: 0 → 1 ``` ```typescript interface UseSlideTransitionReturn { currentIndex: number; // Logical current index displayIndex: number; // Visual index (lags during transition) phase: 'idle' | 'fadingOut' | 'fadingIn'; isTransitioning: boolean; overlayOpacity: number; // 0-1 overlayColor: string; // From settings goToIndex: (index: number) => void; setInitialIndex: (index: number) => void; slideTransitionStyle: CSSProperties; overlayTransitionStyle: CSSProperties; slideOpacity: number; // Current slide visibility } ``` **Usage:** ```typescript import { useSlideTransition } from '../hooks/useSlideTransition'; import { resolveSlideTransition, extractCarouselSlideOverride } from '../lib/resolveSlideTransition'; // Resolve settings with cascade const slideSettings = resolveSlideTransition( pageTransitionSettings, extractCarouselSlideOverride(element), ); const { displayIndex, goToIndex, overlayOpacity, overlayColor, slideTransitionStyle, overlayTransitionStyle, slideOpacity, } = useSlideTransition(slideSettings); // Navigate const handleNext = () => goToIndex((currentIndex + 1) % slides.length); // Render
``` --- > **Note:** Runtime preloading is stream-first: the system preloads current page > assets and outgoing transition videos. --- #### usePageNavigation **File:** `usePageNavigation.ts` (~195 LOC) **Purpose:** Manages page navigation state with optional history tracking. Used by both `RuntimePresentation.tsx` and `constructor.tsx` for unified history management. ```typescript interface UsePageNavigationResult { currentPageId: string | null; currentPage: TPage | null; pageHistory: string[]; previousPageId: string | null; defaultPage: TPage | null; setCurrentPageId: (pageId: string) => void; applyPageSelection: (targetPageId: string, isBack?: boolean) => void; isBackNavigation: (targetPageId: string) => boolean; goBack: () => boolean; resetHistory: () => void; /** Get navigation context for history-based back navigation */ getNavigationContext: () => NavigationContext; } ``` **Key Features:** - **History limit**: `MAX_HISTORY_LENGTH = 50` prevents unbounded growth in long sessions - **Browser-like back navigation**: History pops when `isBack=true` and target matches previous page - **Navigation context**: `getNavigationContext()` provides `{ currentPageSlug, previousPageId }` for `resolveNavigationTarget()` - **Unified state management**: Single source of truth for both constructor and runtime presentation **Usage:** ```typescript // Runtime mode with history (RuntimePresentation.tsx) const { currentPageId: selectedPageId, pageHistory, applyPageSelection, getNavigationContext, } = usePageNavigation({ pages, defaultPageId: initialPageId, trackHistory: true, }); // Constructor mode with history (constructor.tsx) const { currentPageId: activePageId, pageHistory, applyPageSelection, getNavigationContext, setCurrentPageId: setActivePageId, } = usePageNavigation({ pages, trackHistory: true, }); // Navigate with back flag (history pops on back) applyPageSelection(targetPageId, isBack); // Get context for history-based navigation const navContext = getNavigationContext(); const navTarget = resolveNavigationTarget(element, pages, navContext); // Go back programmatically if (nav.goBack()) { console.log('Went back to', nav.previousPageId); } ``` **History Behavior:** ``` Forward: A → B → C → history: [A, B, C] Back to B (isBack=true): → history: [A, B] ✓ Pops C Back to A (isBack=true): → history: [A] ✓ Pops B Forward to D: → history: [A, D] ✓ Adds D ``` --- > **Note:** `useBackgroundTransition` has been consolidated into `usePageNavigationState`. See [usePageNavigationState](#usepagenavigationstate) for the unified state machine. --- #### useBackgroundVideoPlayback **File:** `useBackgroundVideoPlayback.ts` (~225 LOC) **Purpose:** Manages background video playback with custom start/end time control and "play once" functionality for tour pages. ```typescript interface UseBackgroundVideoPlaybackOptions { videoUrl?: string; autoplay?: boolean; // Default: true loop?: boolean; // Default: true muted?: boolean; // Default: true startTime?: number | null; // Start playback at this time (seconds) endTime?: number | null; // Stop/loop at this time (seconds) playOnce?: boolean; // Default: false - play only once per session pageId?: string; // Required for playOnce tracking } interface UseBackgroundVideoPlaybackResult { videoRef: RefObject; } ``` **Key Features:** - Handles `loadedmetadata` event for initial seek to `startTime` - Uses `timeupdate` event to enforce `endTime` boundary - When `endTime` is set, handles looping via JavaScript (seeks back to `startTime`) - Autoplay with browser policy handling (catch and ignore) - **Play Once Mode:** When `playOnce` is true and `pageId` is provided: - Tracks played pages in a module-level `Set` (`playedOncePages`) - On first visit: video plays normally, page ID added to Set on `ended` event - On subsequent visits: video seeks to last frame and pauses immediately - Session-scoped (resets on browser refresh, not persisted to storage) **Usage:** ```typescript const { videoRef } = useBackgroundVideoPlayback({ videoUrl: page.background_video_url, autoplay: page.background_video_autoplay ?? true, loop: page.background_video_loop ?? true, muted: page.background_video_muted ?? true, startTime: page.background_video_start_time ?? null, endTime: page.background_video_end_time ?? null, playOnce: page.background_video_play_once ?? false, pageId: page.id, // Required for play-once tracking }); // Render video with ref