Skip to content

fix: React Hooks & Performance Improvements (exhaustive-deps, ErrorBoundary, memoization)#1182

Open
akhil06232 wants to merge 1 commit intoAOSSIE-Org:mainfrom
akhil06232:fix/react-hooks-performance-improvements
Open

fix: React Hooks & Performance Improvements (exhaustive-deps, ErrorBoundary, memoization)#1182
akhil06232 wants to merge 1 commit intoAOSSIE-Org:mainfrom
akhil06232:fix/react-hooks-performance-improvements

Conversation

@akhil06232
Copy link

@akhil06232 akhil06232 commented Feb 18, 2026

Hey @rahulharpal1603 , This PR fixes some React hooks and performance issues in the frontend.
References Issues #1112 and #937
I turned on the exhaustive-deps lint rule (as a warning) so we can catch missing hook dependencies going forward. I also added an Error Boundary component that wraps the app routes — so if something crashes during rendering, users see a friendly error screen with a reload button instead of a blank white page.

I found and fixed a bug in Sortimage.ts where the useEffect had an empty dependency array but was using data inside it. This meant it would never update when the data changed — basically showing stale results.I also wrapped four components (ImageCard, MediaThumbnails, NavigationButtons, ZoomControls) with React.memo so they don't re-render unnecessarily, especially in image galleries with lots of items. And I added useCallback to two handler functions in useFolderOperationsthat were being recreated on every render.9 files changed, everything compiles cleanly with tsc --noEmit (zero errors).

Summary by CodeRabbit

Release Notes

  • New Features

    • Added comprehensive error handling with user-friendly error screens and recovery options including "Try Again" and "Reload App" buttons.
  • Bug Fixes

    • Fixed an issue where sorted data was not updating when input data changed.
  • Performance

    • Optimized component rendering performance across media and image display components.
  • Chores

    • Updated code quality configuration.

…undary, 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
@github-actions github-actions bot added enhancement New feature or request frontend labels Feb 18, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

Introduces global error boundary for route error handling, wraps performance-sensitive Media components with React.memo for optimization, updates ESLint configuration with stricter React hooks rule enforcement, and refines hook dependency arrays for data freshness.

Changes

Cohort / File(s) Summary
ESLint Configuration
frontend/.eslintrc.json
Reformatted arrays to multi-line format; changed react-hooks/exhaustive-deps rule from "off" to "warn"; no functional behavior change to other rules.
Error Handling
frontend/src/App.tsx, frontend/src/components/ErrorBoundary.tsx
Introduced ErrorBoundary class component with error state management, fallback UI rendering, and recovery actions (Try Again/Reload); wrapped AppRoutes to capture routing errors globally.
Component Memoization
frontend/src/components/Media/ImageCard.tsx, frontend/src/components/Media/MediaThumbnails.tsx, frontend/src/components/Media/NavigationButtons.tsx, frontend/src/components/Media/ZoomControls.tsx
Wrapped all Media components with React.memo to prevent unnecessary re-renders; minor formatting adjustments in className strings; export signatures updated to reflect memoization wrappers.
Hook Optimizations
frontend/src/hooks/Sortimage.ts, frontend/src/hooks/useFolderOperations.tsx
Added [data] dependency to useSortedImages effect for data change reactivity; wrapped toggleAITagging and deleteFolder handlers in useCallback for referential stability.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PR #516: Modifies useFolderOperations.tsx with identical useCallback wrapping of toggleAITagging and deleteFolder handlers.
  • PR #578: Touches ImageCard.tsx and MediaThumbnails.tsx components being memoized in this PR, with overlapping component refactoring.
  • PR #599: Modifies the same frontend Media UI components (ImageCard.tsx, MediaThumbnails.tsx) affected by memoization changes.

Suggested labels

bug

Suggested reviewers

  • rahulharpal1603

Poem

🐰 Whiskers twitching with glee

Errors caught with a boundary's care,
Components memoized through the air,
Hooks now wiser, dependencies clear,
Performance blooms when bugs disappear! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: React Hooks fixes (exhaustive-deps ESLint rule), ErrorBoundary implementation, and memoization optimizations across multiple components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
frontend/src/components/ErrorBoundary.tsx (2)

23-25: componentDidCatch only logs to console — no integration with an error reporting service.

For a production app, this is the ideal hook for forwarding the error to a monitoring/observability service (e.g., Sentry, Datadog). Without it, render crashes in production are silent beyond the local console.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/ErrorBoundary.tsx` around lines 23 - 25, Replace the
console-only behavior in ErrorBoundary.componentDidCatch with forwarding the
error and errorInfo to your monitoring/observability service (e.g., call
Sentry.captureException or Datadog client) and include contextual data
(component stack / errorInfo) and any relevant user/session identifiers; keep
the console.error for local dev but ensure the reporting call is made
(synchronously or awaited in a safe way) and does not throw, so production
render crashes are reported to the external service.

27-29: handleReset silently re-triggers the boundary for persistent render errors.

handleReset clears hasError and re-renders the same child tree. If the underlying error is a persistent render bug (not a transient side-effect), the component immediately throws again and the user is sent right back to the error screen. The "Try Again" label implies the operation might succeed, which is misleading for non-recoverable crashes. Based on learnings, the ErrorBoundary is intended for transient errors — consider adding a guard (e.g., a retry counter) or updating the button label to set accurate expectations.

💡 Example: limit soft-reset attempts
 interface ErrorBoundaryState {
     hasError: boolean;
     error: Error | null;
+    retryCount: number;
 }

 // In constructor:
-    this.state = { hasError: false, error: null };
+    this.state = { hasError: false, error: null, retryCount: 0 };

 // In handleReset:
     handleReset = (): void => {
-        this.setState({ hasError: false, error: null });
+        this.setState((s) => ({
+            hasError: false,
+            error: null,
+            retryCount: s.retryCount + 1,
+        }));
     };

 // In render, disable "Try Again" after N attempts:
-    <button onClick={this.handleReset} ...>
+    <button onClick={this.handleReset} disabled={this.state.retryCount >= 3} ...>
         Try Again
     </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/ErrorBoundary.tsx` around lines 27 - 29, The
handleReset method currently clears hasError and error and immediately
re-renders the same child tree, which rethrows persistent render errors; update
the ErrorBoundary to guard soft resets by adding a retry counter (e.g., state
key retryCount) and a maxRetries constant, increment retryCount in handleReset
and only clear hasError/error if retryCount < maxRetries (otherwise keep the
error state and change UI to show a non-recoverable message or a destructive
fallback like "Reload app"); alternatively update the retry button label based
on retryCount (e.g., "Try Again" while retries remain, then "Reload app / Report
issue") so handleReset, retryCount, hasError, and the render fallback behavior
reflect the new retry limit.
frontend/src/hooks/Sortimage.ts (1)

15-45: Prefer useMemo over useEffect+useState for synchronous data transformation.

parseAndSortImageData is a pure, synchronous function. Using useEffect+useState guarantees a double render on every data change: the first pass renders sortedImages = [], the effect fires post-commit, setSortedImages schedules a second render with the actual data. useMemo computes inline during render and eliminates this empty-state flash entirely.

♻️ Proposed refactor to `useMemo`
-import { useEffect, useMemo, useState } from 'react';
+import { useMemo } from 'react';

 function useSortedImages(data: any): Image[] {
-  const [sortedImages, setSortedImages] = useState<Image[]>([]);
-
-  useEffect(() => {
-    const parseAndSortImageData = (data: any): Image[] => {
-      const images: Image[] = [];
-
-      for (const filePath in data) {
-        if (Object.prototype.hasOwnProperty.call(data, filePath)) {
-          const tags = data[filePath].split(', ');
-          const fileName = filePath.substring(filePath.lastIndexOf('/') + 1);
-
-          const image: Image = {
-            id: fileName,
-            date: '',
-            title: data[filePath],
-            popularity: tags.length,
-            src: filePath,
-            tags: tags,
-          };
-
-          images.push(image);
-        }
-      }
-
-      images.sort((a, b) => b.popularity - a.popularity);
-
-      return images;
-    };
-
-    const sortedImages = parseAndSortImageData(data);
-
-    setSortedImages(sortedImages);
-  }, [data]);
-
-  return sortedImages;
+  return useMemo(() => {
+    if (!data) return [];
+    const images: Image[] = [];
+    for (const filePath in data) {
+      if (Object.prototype.hasOwnProperty.call(data, filePath)) {
+        const tags = data[filePath].split(', ');
+        const fileName = filePath.substring(filePath.lastIndexOf('/') + 1);
+        images.push({
+          id: fileName,
+          date: '',
+          title: data[filePath],
+          popularity: tags.length,
+          src: filePath,
+          tags,
+        });
+      }
+    }
+    images.sort((a, b) => b.popularity - a.popularity);
+    return images;
+  }, [data]);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/hooks/Sortimage.ts` around lines 15 - 45, The current useEffect
block that defines parseAndSortImageData and calls setSortedImages causes an
extra render; replace that pattern with useMemo by computing sortedImages =
useMemo(() => { /* reuse parseAndSortImageData logic or inline it */ }, [data])
and remove the useEffect and the setSortedImages state update; ensure you keep
the same parsing/sorting logic (parseAndSortImageData, tags, fileName,
popularity, images.sort) and depend only on data so sortedImages is memoized and
updated synchronously during render.
frontend/src/App.tsx (1)

29-36: GlobalLoader and InfoDialog render outside ErrorBoundary.

Both components are rendered after the </BrowserRouter> closing tag, leaving them unprotected. If either crashes, the error propagates to the React root uncaught. Given they're Redux-connected UI components, risk is low but worth noting — consider either moving them inside the ErrorBoundary, or wrapping them individually.

💡 Move shared UI inside the boundary
         <BrowserRouter>
           <ErrorBoundary>
             <AppRoutes />
+            <GlobalLoader loading={loading} message={message} />
+            <InfoDialog
+              isOpen={isOpen}
+              title={title}
+              message={infoMessage}
+              variant={variant}
+              showCloseButton={showCloseButton}
+            />
           </ErrorBoundary>
         </BrowserRouter>
-        <GlobalLoader loading={loading} message={message} />
-        <InfoDialog
-          isOpen={isOpen}
-          title={title}
-          message={infoMessage}
-          variant={variant}
-          showCloseButton={showCloseButton}
-        />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.tsx` around lines 29 - 36, Move GlobalLoader and InfoDialog
so they are rendered inside the ErrorBoundary to ensure crashes are caught;
locate the ErrorBoundary wrapper (likely where <ErrorBoundary> surrounds
<BrowserRouter> or route content in App) and either place <GlobalLoader
loading={loading} message={message} /> and the <InfoDialog ... /> JSX before the
closing </ErrorBoundary> tag (instead of after </BrowserRouter>), or wrap each
with the ErrorBoundary component directly (e.g., <ErrorBoundary><GlobalLoader
.../></ErrorBoundary> and <ErrorBoundary><InfoDialog .../></ErrorBoundary>);
update imports/props as needed and verify App renders without moving
BrowserRouter semantics.
frontend/src/components/Media/ImageCard.tsx (2)

5-5: Consider using the named memo import instead of React.memo.

Since React 17+ with the new JSX transform, the React namespace is no longer needed for JSX. The only reason React is imported here is for React.memo. Using the named import keeps the namespace clean and is the idiomatic pattern for modern React codebases.

♻️ Proposed refactor
-import React, { useCallback, useState } from 'react';
+import { memo, useCallback, useState } from 'react';

-export const ImageCard = React.memo(function ImageCard({
+export const ImageCard = memo(function ImageCard({

Also applies to: 20-20, 104-104

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/Media/ImageCard.tsx` at line 5, The import currently
brings in the whole React namespace just to use React.memo; update the import
line to include the named memo (e.g., import React, { useCallback, useState,
memo } from 'react') and then replace any usages of React.memo in this file
(instances around the component export and any other occurrences at the noted
lines) with the named memo identifier (memo(Component)) so the React namespace
is no longer required solely for memoization.

32-36: Narrow useCallback dependency from image to image.id.

The callback body only reads image.id. Depending on the full image object means the callback is recreated whenever any field changes (e.g., isFavourite toggling), which undermines the React.memo wrapper on the parent by propagating a new function reference to any child that receives this callback.

♻️ Proposed fix
-  }, [image, toggleFavourite]);
+  }, [image.id, toggleFavourite]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/Media/ImageCard.tsx` around lines 32 - 36, The
handleToggleFavourite useCallback currently depends on the whole image object
causing unnecessary re-creation; change its dependency to only the image id by
either reading const imageId = image?.id above and using [imageId,
toggleFavourite] or directly using image?.id in the dependency array, ensuring
handleToggleFavourite only depends on image id and toggleFavourite (referencing
the handleToggleFavourite function, useCallback hook, image.id and
toggleFavourite symbols).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/Media/ImageCard.tsx`:
- Line 80: Remove the debug console.log that prints the entire image object in
the ImageCard component; locate the log statement (console.log(image)) inside
the ImageCard's favorite-button click handler (e.g., onFavoriteToggle /
handleFavoriteClick) and delete it so sensitive file paths/metadata are no
longer emitted to the console before merging.

In `@frontend/src/hooks/useFolderOperations.tsx`:
- Around line 147-160: The callbacks toggleAITagging and deleteFolder currently
depend on whole mutation objects (enableAITaggingMutation,
disableAITaggingMutation, deleteFolderMutation) which are unstable; instead
destructure and capture the stable .mutate functions (e.g., const { mutate:
enableAITagging } = enableAITaggingMutation, const { mutate: disableAITagging }
= disableAITaggingMutation, const { mutate: deleteFolder } =
deleteFolderMutation) and update the useCallback bodies to call those mutate
functions, then list only those mutate symbols (enableAITagging,
disableAITagging, deleteFolder) in the dependency arrays for toggleAITagging and
deleteFolder respectively so the callbacks remain stable.

---

Nitpick comments:
In `@frontend/src/App.tsx`:
- Around line 29-36: Move GlobalLoader and InfoDialog so they are rendered
inside the ErrorBoundary to ensure crashes are caught; locate the ErrorBoundary
wrapper (likely where <ErrorBoundary> surrounds <BrowserRouter> or route content
in App) and either place <GlobalLoader loading={loading} message={message} />
and the <InfoDialog ... /> JSX before the closing </ErrorBoundary> tag (instead
of after </BrowserRouter>), or wrap each with the ErrorBoundary component
directly (e.g., <ErrorBoundary><GlobalLoader .../></ErrorBoundary> and
<ErrorBoundary><InfoDialog .../></ErrorBoundary>); update imports/props as
needed and verify App renders without moving BrowserRouter semantics.

In `@frontend/src/components/ErrorBoundary.tsx`:
- Around line 23-25: Replace the console-only behavior in
ErrorBoundary.componentDidCatch with forwarding the error and errorInfo to your
monitoring/observability service (e.g., call Sentry.captureException or Datadog
client) and include contextual data (component stack / errorInfo) and any
relevant user/session identifiers; keep the console.error for local dev but
ensure the reporting call is made (synchronously or awaited in a safe way) and
does not throw, so production render crashes are reported to the external
service.
- Around line 27-29: The handleReset method currently clears hasError and error
and immediately re-renders the same child tree, which rethrows persistent render
errors; update the ErrorBoundary to guard soft resets by adding a retry counter
(e.g., state key retryCount) and a maxRetries constant, increment retryCount in
handleReset and only clear hasError/error if retryCount < maxRetries (otherwise
keep the error state and change UI to show a non-recoverable message or a
destructive fallback like "Reload app"); alternatively update the retry button
label based on retryCount (e.g., "Try Again" while retries remain, then "Reload
app / Report issue") so handleReset, retryCount, hasError, and the render
fallback behavior reflect the new retry limit.

In `@frontend/src/components/Media/ImageCard.tsx`:
- Line 5: The import currently brings in the whole React namespace just to use
React.memo; update the import line to include the named memo (e.g., import
React, { useCallback, useState, memo } from 'react') and then replace any usages
of React.memo in this file (instances around the component export and any other
occurrences at the noted lines) with the named memo identifier (memo(Component))
so the React namespace is no longer required solely for memoization.
- Around line 32-36: The handleToggleFavourite useCallback currently depends on
the whole image object causing unnecessary re-creation; change its dependency to
only the image id by either reading const imageId = image?.id above and using
[imageId, toggleFavourite] or directly using image?.id in the dependency array,
ensuring handleToggleFavourite only depends on image id and toggleFavourite
(referencing the handleToggleFavourite function, useCallback hook, image.id and
toggleFavourite symbols).

In `@frontend/src/hooks/Sortimage.ts`:
- Around line 15-45: The current useEffect block that defines
parseAndSortImageData and calls setSortedImages causes an extra render; replace
that pattern with useMemo by computing sortedImages = useMemo(() => { /* reuse
parseAndSortImageData logic or inline it */ }, [data]) and remove the useEffect
and the setSortedImages state update; ensure you keep the same parsing/sorting
logic (parseAndSortImageData, tags, fileName, popularity, images.sort) and
depend only on data so sortedImages is memoized and updated synchronously during
render.

}`}
}`}
onClick={(e) => {
console.log(image);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove debug console.log before merging.

This statement logs the full image object (containing file paths and metadata) to the console on every favourite-button click. It's a debug artifact that should not ship.

🐛 Proposed fix
             onClick={(e) => {
-              console.log(image);
               e.stopPropagation();
               handleToggleFavourite();
             }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log(image);
onClick={(e) => {
e.stopPropagation();
handleToggleFavourite();
}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/Media/ImageCard.tsx` at line 80, Remove the debug
console.log that prints the entire image object in the ImageCard component;
locate the log statement (console.log(image)) inside the ImageCard's
favorite-button click handler (e.g., onFavoriteToggle / handleFavoriteClick) and
delete it so sensitive file paths/metadata are no longer emitted to the console
before merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request frontend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments