From 0d94a6c57f33aaeed5dc9cc72a4f59cf10ee94f7 Mon Sep 17 00:00:00 2001 From: akhil06232 Date: Thu, 19 Feb 2026 01:24:25 +0530 Subject: [PATCH] fix: react hooks & performance improvements (exhaustive-deps, ErrorBoundary, memoization) - Enable react-hooks/exhaustive-deps ESLint rule as warn - Add ErrorBoundary component to catch render errors gracefully - Fix useSortedImages stale closure bug (missing data dependency) - Memoize ImageCard, MediaThumbnails, NavigationButtons, ZoomControls with React.memo - Wrap toggleAITagging and deleteFolder with useCallback in useFolderOperations Closes AOSSIE-Org/PictoPy issue 1112 Closes AOSSIE-Org/PictoPy issue 937 --- frontend/.eslintrc.json | 21 ++- frontend/src/App.tsx | 5 +- frontend/src/components/ErrorBoundary.tsx | 143 ++++++++++++++++++ frontend/src/components/Media/ImageCard.tsx | 11 +- .../src/components/Media/MediaThumbnails.tsx | 18 +-- .../components/Media/NavigationButtons.tsx | 4 +- .../src/components/Media/ZoomControls.tsx | 9 +- frontend/src/hooks/Sortimage.ts | 2 +- frontend/src/hooks/useFolderOperations.tsx | 10 +- 9 files changed, 188 insertions(+), 35 deletions(-) create mode 100644 frontend/src/components/ErrorBoundary.tsx diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index d57fbde75..6e48061c1 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -1,6 +1,11 @@ { - "extends": ["react-app", "eslint-config-prettier"], - "ignorePatterns": ["src-tauri/target"], + "extends": [ + "react-app", + "eslint-config-prettier" + ], + "ignorePatterns": [ + "src-tauri/target" + ], "rules": { // Accessibility rules "jsx-a11y/click-events-have-key-events": "off", @@ -11,11 +16,17 @@ "jsx-a11y/anchor-has-content": "off", "jsx-a11y/media-has-caption": "off", // React Hook rules - "react-hooks/exhaustive-deps": "off", + "react-hooks/exhaustive-deps": "warn", // Temporarily turn warnings into errors to suppress them "no-warning-comments": [ "error", - { "terms": ["todo", "fixme"], "location": "anywhere" } + { + "terms": [ + "todo", + "fixme" + ], + "location": "anywhere" + } ] } -} +} \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2c5b6bddb..d176aa295 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import { ThemeProvider } from '@/contexts/ThemeContext'; import QueryClientProviders from '@/config/QueryClientProvider'; import { GlobalLoader } from './components/Loader/GlobalLoader'; import { InfoDialog } from './components/Dialog/InfoDialog'; +import ErrorBoundary from './components/ErrorBoundary'; import { useSelector } from 'react-redux'; import { RootState } from './app/store'; const App: React.FC = () => { @@ -21,7 +22,9 @@ const App: React.FC = () => { - + + + { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + console.error('ErrorBoundary caught an error:', error, errorInfo); + } + + handleReset = (): void => { + this.setState({ hasError: false, error: null }); + }; + + handleReload = (): void => { + window.location.reload(); + }; + + render(): ReactNode { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( +
+
+

+ Something went wrong +

+

+ An unexpected error occurred. You can try again or reload the + application. +

+ {this.state.error && ( +
+                                {this.state.error.message}
+                            
+ )} +
+ + +
+
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/components/Media/ImageCard.tsx b/frontend/src/components/Media/ImageCard.tsx index 0cc6a715a..f8cdd0a9f 100644 --- a/frontend/src/components/Media/ImageCard.tsx +++ b/frontend/src/components/Media/ImageCard.tsx @@ -2,7 +2,7 @@ import { AspectRatio } from '@/components/ui/aspect-ratio'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { Check, Heart } from 'lucide-react'; -import { useCallback, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { Image } from '@/types/Media'; import { ImageTags } from './ImageTags'; import { convertFileSrc } from '@tauri-apps/api/core'; @@ -17,7 +17,7 @@ interface ImageCardViewProps { imageIndex?: number; } -export function ImageCard({ +export const ImageCard = React.memo(function ImageCard({ image, className, isSelected = false, @@ -72,11 +72,10 @@ export function ImageCard({ ); -}; +}); diff --git a/frontend/src/components/Media/ZoomControls.tsx b/frontend/src/components/Media/ZoomControls.tsx index 9a4eeafff..27dd417c0 100644 --- a/frontend/src/components/Media/ZoomControls.tsx +++ b/frontend/src/components/Media/ZoomControls.tsx @@ -9,7 +9,7 @@ interface ZoomControlsProps { showThumbnails: boolean; } -export const ZoomControls: React.FC = ({ +export const ZoomControls: React.FC = React.memo(({ onZoomIn, onZoomOut, onRotate, @@ -18,9 +18,8 @@ export const ZoomControls: React.FC = ({ }) => { return (
); -}; +}); diff --git a/frontend/src/hooks/Sortimage.ts b/frontend/src/hooks/Sortimage.ts index b17052195..5fa0d3583 100644 --- a/frontend/src/hooks/Sortimage.ts +++ b/frontend/src/hooks/Sortimage.ts @@ -42,7 +42,7 @@ function useSortedImages(data: any): Image[] { const sortedImages = parseAndSortImageData(data); setSortedImages(sortedImages); - }, []); + }, [data]); return sortedImages; } diff --git a/frontend/src/hooks/useFolderOperations.tsx b/frontend/src/hooks/useFolderOperations.tsx index ff747ff0b..f1579713a 100644 --- a/frontend/src/hooks/useFolderOperations.tsx +++ b/frontend/src/hooks/useFolderOperations.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { usePictoMutation, usePictoQuery } from '@/hooks/useQueryExtension'; import { @@ -144,20 +144,20 @@ export const useFolderOperations = () => { /** * Toggle AI tagging for a folder */ - const toggleAITagging = (folder: FolderDetails) => { + const toggleAITagging = useCallback((folder: FolderDetails) => { if (folder.AI_Tagging) { disableAITaggingMutation.mutate(folder.folder_id); } else { enableAITaggingMutation.mutate(folder.folder_id); } - }; + }, [enableAITaggingMutation, disableAITaggingMutation]); /** * Delete a folder */ - const deleteFolder = (folderId: string) => { + const deleteFolder = useCallback((folderId: string) => { deleteFolderMutation.mutate(folderId); - }; + }, [deleteFolderMutation]); return { // Data