From 57b2fcaf2c06e804c98d5596c7f5205bfeee54f1 Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Fri, 26 Sep 2025 11:58:03 +0200 Subject: [PATCH] Refactor to use MutableRefObject instead of RefObject Replaces all instances of React.RefObject with React.MutableRefObject for refs that are mutated directly throughout the codebase. This change clarifies intent and improves type safety for refs that are updated outside of React's ref assignment. --- .../src/components/pageEditor/PageThumbnail.tsx | 2 +- frontend/src/components/tools/ToolPicker.tsx | 2 +- frontend/src/contexts/FileManagerContext.tsx | 2 +- frontend/src/contexts/file/fileActions.ts | 14 +++++++------- frontend/src/contexts/file/fileSelectors.ts | 8 ++++---- frontend/src/contexts/file/lifecycle.ts | 12 ++++++------ frontend/src/hooks/useIsOverflowing.ts | 12 ++++++------ frontend/src/hooks/useTooltipPosition.ts | 4 ++-- frontend/src/types/sidebar.ts | 4 ++-- 9 files changed, 30 insertions(+), 30 deletions(-) diff --git a/frontend/src/components/pageEditor/PageThumbnail.tsx b/frontend/src/components/pageEditor/PageThumbnail.tsx index b8cdb182c..dfa098af1 100644 --- a/frontend/src/components/pageEditor/PageThumbnail.tsx +++ b/frontend/src/components/pageEditor/PageThumbnail.tsx @@ -23,7 +23,7 @@ interface PageThumbnailProps { selectionMode: boolean; movingPage: number | null; isAnimating: boolean; - pageRefs: React.RefObject>; + pageRefs: React.MutableRefObject>; onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void; onTogglePage: (pageId: string) => void; onAnimateReorder: () => void; diff --git a/frontend/src/components/tools/ToolPicker.tsx b/frontend/src/components/tools/ToolPicker.tsx index 16f18265c..37be8ed5a 100644 --- a/frontend/src/components/tools/ToolPicker.tsx +++ b/frontend/src/components/tools/ToolPicker.tsx @@ -71,7 +71,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa [visibleSections] ); - const scrollTo = (ref: React.RefObject) => { + const scrollTo = (ref: React.MutableRefObject) => { const container = scrollableRef.current; const target = ref.current; if (container && target) { diff --git a/frontend/src/contexts/FileManagerContext.tsx b/frontend/src/contexts/FileManagerContext.tsx index 6f3afe21b..98b098b38 100644 --- a/frontend/src/contexts/FileManagerContext.tsx +++ b/frontend/src/contexts/FileManagerContext.tsx @@ -13,7 +13,7 @@ interface FileManagerContextValue { searchTerm: string; selectedFiles: StirlingFileStub[]; filteredFiles: StirlingFileStub[]; - fileInputRef: React.RefObject; + fileInputRef: React.MutableRefObject; selectedFilesSet: Set; expandedFileIds: Set; fileGroups: Map; diff --git a/frontend/src/contexts/file/fileActions.ts b/frontend/src/contexts/file/fileActions.ts index 070ef5474..1cb38ed53 100644 --- a/frontend/src/contexts/file/fileActions.ts +++ b/frontend/src/contexts/file/fileActions.ts @@ -165,8 +165,8 @@ interface AddFileOptions { */ export async function addFiles( options: AddFileOptions, - stateRef: React.RefObject, - filesRef: React.RefObject>, + stateRef: React.MutableRefObject, + filesRef: React.MutableRefObject>, dispatch: React.Dispatch, lifecycleManager: FileLifecycleManager, enablePersistence: boolean = false @@ -278,7 +278,7 @@ export async function consumeFiles( inputFileIds: FileId[], outputStirlingFiles: StirlingFile[], outputStirlingFileStubs: StirlingFileStub[], - filesRef: React.RefObject>, + filesRef: React.MutableRefObject>, dispatch: React.Dispatch ): Promise { if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputStirlingFiles.length} output files with pre-created stubs`); @@ -357,7 +357,7 @@ export async function consumeFiles( async function restoreFilesAndCleanup( filesToRestore: { file: File; record: StirlingFileStub }[], fileIdsToRemove: FileId[], - filesRef: React.RefObject>, + filesRef: React.MutableRefObject>, indexedDB?: { deleteFile: (fileId: FileId) => Promise } | null ): Promise { // Remove files from filesRef @@ -406,7 +406,7 @@ export async function undoConsumeFiles( inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[], - filesRef: React.RefObject>, + filesRef: React.MutableRefObject>, dispatch: React.Dispatch, indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise; deleteFile: (fileId: FileId) => Promise } | null ): Promise { @@ -468,8 +468,8 @@ export async function undoConsumeFiles( export async function addStirlingFileStubs( stirlingFileStubs: StirlingFileStub[], options: { insertAfterPageId?: string; selectFiles?: boolean } = {}, - stateRef: React.RefObject, - filesRef: React.RefObject>, + stateRef: React.MutableRefObject, + filesRef: React.MutableRefObject>, dispatch: React.Dispatch, _lifecycleManager: FileLifecycleManager ): Promise { diff --git a/frontend/src/contexts/file/fileSelectors.ts b/frontend/src/contexts/file/fileSelectors.ts index dd975601e..c0a6e987f 100644 --- a/frontend/src/contexts/file/fileSelectors.ts +++ b/frontend/src/contexts/file/fileSelectors.ts @@ -15,8 +15,8 @@ import { * Create stable selectors using stateRef and filesRef */ export function createFileSelectors( - stateRef: React.RefObject, - filesRef: React.RefObject> + stateRef: React.MutableRefObject, + filesRef: React.MutableRefObject> ): FileContextSelectors { return { getFile: (id: FileId) => { @@ -125,8 +125,8 @@ export function buildQuickKeySetFromMetadata(metadata: { name: string; size: num * Get primary file (first in list) - commonly used pattern */ export function getPrimaryFile( - stateRef: React.RefObject, - filesRef: React.RefObject> + stateRef: React.MutableRefObject, + filesRef: React.MutableRefObject> ): { file?: File; record?: StirlingFileStub } { const primaryFileId = stateRef.current.files.ids[0]; if (!primaryFileId) return {}; diff --git a/frontend/src/contexts/file/lifecycle.ts b/frontend/src/contexts/file/lifecycle.ts index 3c95fe788..c65fec127 100644 --- a/frontend/src/contexts/file/lifecycle.ts +++ b/frontend/src/contexts/file/lifecycle.ts @@ -16,7 +16,7 @@ export class FileLifecycleManager { private fileGenerations = new Map(); // Generation tokens to prevent stale cleanup constructor( - private filesRef: React.RefObject>, + private filesRef: React.MutableRefObject>, private dispatch: React.Dispatch ) {} @@ -34,7 +34,7 @@ export class FileLifecycleManager { /** * Clean up resources for a specific file (with stateRef access for complete cleanup) */ - cleanupFile = (fileId: FileId, stateRef?: React.RefObject): void => { + cleanupFile = (fileId: FileId, stateRef?: React.MutableRefObject): void => { // Use comprehensive cleanup (same as removeFiles) this.cleanupAllResourcesForFile(fileId, stateRef); @@ -68,7 +68,7 @@ export class FileLifecycleManager { /** * Schedule delayed cleanup for a file with generation token to prevent stale cleanup */ - scheduleCleanup = (fileId: FileId, delay: number = 30000, stateRef?: React.RefObject): void => { + scheduleCleanup = (fileId: FileId, delay: number = 30000, stateRef?: React.MutableRefObject): void => { // Cancel existing timer const existingTimer = this.cleanupTimers.get(fileId); if (existingTimer) { @@ -101,7 +101,7 @@ export class FileLifecycleManager { /** * Remove a file immediately with complete resource cleanup */ - removeFiles = (fileIds: FileId[], stateRef?: React.RefObject): void => { + removeFiles = (fileIds: FileId[], stateRef?: React.MutableRefObject): void => { fileIds.forEach(fileId => { // Clean up all resources for this file this.cleanupAllResourcesForFile(fileId, stateRef); @@ -114,7 +114,7 @@ export class FileLifecycleManager { /** * Complete resource cleanup for a single file */ - private cleanupAllResourcesForFile = (fileId: FileId, stateRef?: React.RefObject): void => { + private cleanupAllResourcesForFile = (fileId: FileId, stateRef?: React.MutableRefObject): void => { // Remove from files ref this.filesRef.current.delete(fileId); @@ -166,7 +166,7 @@ export class FileLifecycleManager { /** * Update file record with race condition guards */ - updateStirlingFileStub = (fileId: FileId, updates: Partial, stateRef?: React.RefObject): void => { + updateStirlingFileStub = (fileId: FileId, updates: Partial, stateRef?: React.MutableRefObject): void => { // Guard against updating removed files (race condition protection) if (!this.filesRef.current.has(fileId)) { if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`); diff --git a/frontend/src/hooks/useIsOverflowing.ts b/frontend/src/hooks/useIsOverflowing.ts index b5e6d3962..66732f02d 100644 --- a/frontend/src/hooks/useIsOverflowing.ts +++ b/frontend/src/hooks/useIsOverflowing.ts @@ -32,7 +32,7 @@ import * as React from 'react'; */ -export const useIsOverflowing = (ref: React.RefObject, callback?: (isOverflow: boolean) => void) => { +export const useIsOverflowing = (ref: React.MutableRefObject, callback?: (isOverflow: boolean) => void) => { // State to track overflow status const [isOverflow, setIsOverflow] = React.useState(undefined); @@ -42,11 +42,11 @@ export const useIsOverflowing = (ref: React.RefObject, callb // Function to check if element is overflowing const trigger = () => { if (!current) return; - + // Compare scroll height (total content height) vs client height (visible height) const hasOverflow = current.scrollHeight > current.clientHeight; setIsOverflow(hasOverflow); - + // Call optional callback with overflow state if (callback) callback(hasOverflow); }; @@ -56,13 +56,13 @@ export const useIsOverflowing = (ref: React.RefObject, callb if ('ResizeObserver' in window) { const resizeObserver = new ResizeObserver(trigger); resizeObserver.observe(current); - + // Cleanup function to disconnect observer return () => { resizeObserver.disconnect(); }; } - + // Fallback for browsers without ResizeObserver support // Add a small delay to ensure the element is fully rendered setTimeout(trigger, 0); @@ -70,4 +70,4 @@ export const useIsOverflowing = (ref: React.RefObject, callb }, [callback, ref]); return isOverflow; -}; \ No newline at end of file +}; diff --git a/frontend/src/hooks/useTooltipPosition.ts b/frontend/src/hooks/useTooltipPosition.ts index 51ef2e9c3..104062fe2 100644 --- a/frontend/src/hooks/useTooltipPosition.ts +++ b/frontend/src/hooks/useTooltipPosition.ts @@ -60,8 +60,8 @@ export function useTooltipPosition({ sidebarTooltip: boolean; position: Position; gap: number; - triggerRef: React.RefObject; - tooltipRef: React.RefObject; + triggerRef: React.MutableRefObject; + tooltipRef: React.MutableRefObject; sidebarRefs?: SidebarRefs; sidebarState?: SidebarState; }): PositionState { diff --git a/frontend/src/types/sidebar.ts b/frontend/src/types/sidebar.ts index b93db61d6..be98518c5 100644 --- a/frontend/src/types/sidebar.ts +++ b/frontend/src/types/sidebar.ts @@ -5,8 +5,8 @@ export interface SidebarState { } export interface SidebarRefs { - quickAccessRef: React.RefObject; - toolPanelRef: React.RefObject; + quickAccessRef: React.MutableRefObject; + toolPanelRef: React.MutableRefObject; } export interface SidebarInfo {