Skip to content

fix: eliminate setState-in-effect anti-patterns (Category D, issue #200)#256

Open
R-Hart80 wants to merge 4 commits into
CatholicOS:devfrom
R-Hart80:fix/setState-in-effect-category-d
Open

fix: eliminate setState-in-effect anti-patterns (Category D, issue #200)#256
R-Hart80 wants to merge 4 commits into
CatholicOS:devfrom
R-Hart80:fix/setState-in-effect-category-d

Conversation

@R-Hart80
Copy link
Copy Markdown
Contributor

@R-Hart80 R-Hart80 commented May 14, 2026

Fixes part of #200Category D: direct setState calls inside useEffect bodies that can be replaced with lazy initialisers or derived state.

Changes

File Before After
components/editor/shared/EntityTreeToolbar.tsx useState(false) + mount effect reads localStorage and calls setShowTip(true) useState lazy initialiser reads localStorage synchronously; useEffect removed entirely
app/auth/error/page.tsx setRetryCount((c) => c + 1) + retry() called directly in effect body when countdown hits 0 redundant setRetryCount removed; retry() wrapped in setTimeout callback so it runs asynchronously (same pattern as the existing setCountdown tick)
app/settings/page.tsx useState(null) + mount effect reads window.location.hash and calls setHighlightedSetting(hash) useState lazy initialiser reads hash synchronously; effect now only handles scroll + timer to clear
app/page.tsx useRef(false) + useEffect sets filter("mine") once when session resolves userFilter state starts as null; filter is derived via null-coalesce (userFilter ?? (authenticated ? "mine" : "public")); removes ref and effect entirely

Verification

  • npx tsc --noEmit — only pre-existing LanguagePicker.tsx errors (missing cmdk)
  • npm run test:coverage -- --run — 2737 tests pass, 0 regressions
  • npx eslint on the 4 changed files — 0 warnings (down from 4)
  • Remaining react-hooks/set-state-in-effect warnings in the codebase are pre-existing Category A/B/C sites (out of scope for this PR)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Search queries now highlight matching ancestor nodes in the tree view.
  • Bug Fixes

    • Settings page deep links now load with correct preferences highlighted.
  • Style

    • Improved visual feedback for read-only form fields in project settings.
  • Tests

    • Added test coverage for search-match highlighting behavior in tree filtering.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Warning

Review limit reached

@R-Hart80, we couldn't start this review because you've used your available PR reviews for now.

Your plan currently allows 1 review/hour. Refill in 58 minutes and 37 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ec78ebc-261e-4630-ab16-317a850bbe4c

📥 Commits

Reviewing files that changed from the base of the PR and between 157b859 and 4efc369.

📒 Files selected for processing (9)
  • __tests__/lib/hooks/useFilteredTree.test.ts
  • app/auth/error/page.tsx
  • app/page.tsx
  • app/projects/[id]/settings/page.tsx
  • app/settings/page.tsx
  • components/editor/developer/DeveloperEditorLayout.tsx
  • components/editor/shared/EntityTreeToolbar.tsx
  • components/editor/standard/StandardEditorLayout.tsx
  • lib/hooks/useFilteredTree.ts
📝 Walkthrough

Walkthrough

The PR extends search functionality in the filtered-tree hook to mark ancestor nodes matching the query by label (case-insensitive), integrates that query through editor layouts, adds test coverage, and refactors state initialization patterns across several components to use derived values or lazy initializers instead of effects.

Changes

Feature and refactor updates

Layer / File(s) Summary
Search query ancestor highlighting in useFilteredTree
lib/hooks/useFilteredTree.ts
The useFilteredTree hook now accepts an optional searchQuery and passes it to mergePathsIntoTree, which marks ancestor nodes whose labels contain the normalized query (case-insensitive) as isSearchMatch, extending highlighting beyond backend-matched IRIs.
Pass searchQuery through editor layouts
components/editor/developer/DeveloperEditorLayout.tsx, components/editor/standard/StandardEditorLayout.tsx
Both editor layouts now include searchQuery when calling useFilteredTree, enabling ancestor label-matching in the filtered tree.
Test ancestor label-matching behavior
__tests__/lib/hooks/useFilteredTree.test.ts
Four new test cases verify ancestor highlighting: matching occurs when labels contain the query (case-insensitive), does not trigger for empty queries, and respects label-content boundaries.
State initialization refactors
app/page.tsx, app/settings/page.tsx, components/editor/shared/EntityTreeToolbar.tsx
Three components move from useEffect-based state updates to derived values or lazy initializers: HomePage derives filter from auth status, EditorPreferencesSection reads hash during init, and EntityTreeToolbar initializes localStorage state without an effect.
Project settings field styling and layout
app/projects/[id]/settings/page.tsx
Repository field labels (owner, name, branch, file-path) conditionally apply opacity-60 when webhook-driven, and the webhook-information paragraph is repositioned later in the form.
Defer transient error retry
app/auth/error/page.tsx
Error countdown retry is now deferred via zero-delay setTimeout with cleanup, instead of immediate invocation.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • CatholicOS/ontokit-web#200: Several edits implement the "set-state-in-effect" refactors (app/auth/error/page.tsx, app/page.tsx, app/settings/page.tsx, components/editor/shared/EntityTreeToolbar.tsx).
  • CatholicOS/ontokit-web#209: Both directly touch useFilteredTree and mergePathsIntoTree to enable query-based ancestor label matching.

Possibly related PRs

  • CatholicOS/ontokit-web#95: Both PRs modify useFilteredTree and mergePathsIntoTree search-matching logic and extend test coverage around isSearchMatch propagation for ancestor nodes.

Suggested reviewers

  • JohnRDOrazio

Poem

🐰 A rabbit hops through query trees,
Marking ancestors with ease,
State flows clear without effect's dance,
Settings styled with timely glance.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: eliminate setState-in-effect anti-patterns (Category D, issue #200)' clearly and specifically summarizes the main objective of the pull request—refactoring useState-in-effect patterns across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 14, 2026

Codecov Report

❌ Patch coverage is 88.88889% with 1 line in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
components/editor/shared/EntityTreeToolbar.tsx 66.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/hooks/useFilteredTree.ts (1)

112-112: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add searchQuery to the useEffect dependency array.

The effect uses searchQuery at line 97 (mergePathsIntoTree(ancestorPaths, searchQuery ?? "")), but it is not included in the dependency array at line 112. If searchQuery changes without searchResults changing, the effect won't rerun and label-based highlighting will become stale.

Fix
-  }, [searchResults, projectId, accessToken, branch]);
+  }, [searchResults, projectId, accessToken, branch, searchQuery]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/hooks/useFilteredTree.ts` at line 112, The useEffect in
useFilteredTree.ts is missing searchQuery in its dependency array causing stale
highlights when searchQuery changes; update the dependency array (currently
[searchResults, projectId, accessToken, branch]) to include searchQuery so the
effect that calls mergePathsIntoTree(ancestorPaths, searchQuery ?? "") reruns on
searchQuery updates.
🧹 Nitpick comments (1)
app/projects/[id]/settings/page.tsx (1)

2985-2989: 💤 Low value

Consider relocating the informational message before the read-only fields.

The explanatory text "Repository fields are managed by the GitHub integration" appears after all four fields. Users will encounter the dimmed, read-only fields before seeing this explanation.

For optimal UX, consider moving this message to appear immediately before the first read-only field (Repository owner) or as a banner at the top of the grid section, so users understand the constraint before attempting to interact with the fields.

💡 Suggested repositioning

Move the informational message before the repository fields grid:

         Enable remote tracking
       </span>
     </label>

+    {isWebhookDriven && (
+      <p className="text-xs text-indigo-500 dark:text-indigo-400">
+        Repository fields are managed by the GitHub integration.
+      </p>
+    )}
+
     {/* Repository */}
     <div className="grid grid-cols-2 gap-3">
       <div>
         <label className={cn(
           "mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300",
           isWebhookDriven && "opacity-60"
         )}>
           Repository owner
         </label>
         ...
       </div>
     </div>
     ...
-    {isWebhookDriven && (
-      <p className="text-xs text-indigo-500 dark:text-indigo-400">
-        Repository fields are managed by the GitHub integration.
-      </p>
-    )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/projects/`[id]/settings/page.tsx around lines 2985 - 2989, Move the
explanatory text so users see it before encountering read-only fields: when
isWebhookDriven is true, render the paragraph with "Repository fields are
managed by the GitHub integration" immediately before the repository fields grid
(i.e., before the JSX that renders the "Repository owner" input and the other
three repository fields) or as a banner at the top of that grid section; update
the conditional that uses isWebhookDriven to wrap/precede the repository fields
container instead of appearing after the four fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/projects/`[id]/settings/page.tsx:
- Around line 2886-2889: The label rendering using cn(...) with isWebhookDriven
currently applies "opacity-60" which drops the light-mode contrast below WCAG
AA; update the class logic in the label elements (the <label> usages that call
cn with isWebhookDriven — e.g., the Repository owner/name, Branch, and File path
labels) so that when isWebhookDriven is true you either use a higher-contrast
text class (e.g., "text-slate-600") in light mode or increase opacity (e.g.,
"opacity-75"), or apply "opacity-60" only for dark mode; implement this by
changing the conditional passed to cn(...) (reference the isWebhookDriven
conditional in page.tsx) to choose between "text-slate-600" or "opacity-75" for
light mode and keep "opacity-60" for dark mode via a dark: utility or two-branch
conditional.

---

Outside diff comments:
In `@lib/hooks/useFilteredTree.ts`:
- Line 112: The useEffect in useFilteredTree.ts is missing searchQuery in its
dependency array causing stale highlights when searchQuery changes; update the
dependency array (currently [searchResults, projectId, accessToken, branch]) to
include searchQuery so the effect that calls mergePathsIntoTree(ancestorPaths,
searchQuery ?? "") reruns on searchQuery updates.

---

Nitpick comments:
In `@app/projects/`[id]/settings/page.tsx:
- Around line 2985-2989: Move the explanatory text so users see it before
encountering read-only fields: when isWebhookDriven is true, render the
paragraph with "Repository fields are managed by the GitHub integration"
immediately before the repository fields grid (i.e., before the JSX that renders
the "Repository owner" input and the other three repository fields) or as a
banner at the top of that grid section; update the conditional that uses
isWebhookDriven to wrap/precede the repository fields container instead of
appearing after the four fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e34d7c7-e076-41fb-a54a-231fc8dde8b6

📥 Commits

Reviewing files that changed from the base of the PR and between a130901 and 157b859.

📒 Files selected for processing (9)
  • __tests__/lib/hooks/useFilteredTree.test.ts
  • app/auth/error/page.tsx
  • app/page.tsx
  • app/projects/[id]/settings/page.tsx
  • app/settings/page.tsx
  • components/editor/developer/DeveloperEditorLayout.tsx
  • components/editor/shared/EntityTreeToolbar.tsx
  • components/editor/standard/StandardEditorLayout.tsx
  • lib/hooks/useFilteredTree.ts

Comment on lines +2886 to +2889
<label className={cn(
"mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300",
isWebhookDriven && "opacity-60"
)}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check WCAG contrast ratio for dimmed labels
# The script calculates the effective color after applying opacity-60 to slate-700

python3 << 'EOF'
def hex_to_rgb(hex_color):
    hex_color = hex_color.lstrip('#')
    return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))

def apply_opacity(fg_rgb, bg_rgb, opacity):
    """Blend foreground with background at given opacity"""
    return tuple(
        int(fg * opacity + bg * (1 - opacity))
        for fg, bg in zip(fg_rgb, bg_rgb)
    )

def relative_luminance(rgb):
    """Calculate relative luminance per WCAG formula"""
    r, g, b = [x / 255.0 for x in rgb]
    r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4
    g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4
    b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4
    return 0.2126 * r + 0.7152 * g + 0.0722 * b

def contrast_ratio(l1, l2):
    """Calculate contrast ratio between two luminance values"""
    lighter = max(l1, l2)
    darker = min(l1, l2)
    return (lighter + 0.05) / (darker + 0.05)

# Tailwind slate-700 (approximate)
slate_700 = (51, 65, 85)
white = (255, 255, 255)

# Apply 60% opacity
effective_color = apply_opacity(slate_700, white, 0.6)
print(f"Slate-700 at 60% opacity: rgb{effective_color}")

# Calculate contrast
lum_effective = relative_luminance(effective_color)
lum_white = relative_luminance(white)
ratio = contrast_ratio(lum_effective, lum_white)

print(f"Contrast ratio: {ratio:.2f}:1")
print(f"WCAG AA normal text (4.5:1): {'PASS' if ratio >= 4.5 else 'FAIL'}")
print(f"WCAG AA large text (3:1): {'PASS' if ratio >= 3.0 else 'FAIL'}")
print(f"WCAG AAA normal text (7:1): {'PASS' if ratio >= 7.0 else 'FAIL'}")
EOF

Repository: CatholicOS/ontokit-web

Length of output: 232


🏁 Script executed:

sed -n '2880,2895p' app/projects/[id]/settings/page.tsx | head -20

Repository: CatholicOS/ontokit-web

Length of output: 653


🏁 Script executed:

python3 << 'EOF'
# Check dark mode contrast as well
def apply_opacity(fg_rgb, bg_rgb, opacity):
    """Blend foreground with background at given opacity"""
    return tuple(
        int(fg * opacity + bg * (1 - opacity))
        for fg, bg in zip(fg_rgb, bg_rgb)
    )

def relative_luminance(rgb):
    """Calculate relative luminance per WCAG formula"""
    r, g, b = [x / 255.0 for x in rgb]
    r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4
    g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4
    b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4
    return 0.2126 * r + 0.7152 * g + 0.0722 * b

def contrast_ratio(l1, l2):
    """Calculate contrast ratio between two luminance values"""
    lighter = max(l1, l2)
    darker = min(l1, l2)
    return (lighter + 0.05) / (darker + 0.05)

# Dark mode: slate-300 on dark background (approximate dark background: `#1e293b` or rgb(30, 41, 59))
slate_300 = (203, 213, 225)
dark_bg = (30, 41, 59)

# Apply 60% opacity on dark background
effective_color_dark = apply_opacity(slate_300, dark_bg, 0.6)
print(f"Dark mode: Slate-300 at 60% opacity on dark background: rgb{effective_color_dark}")

# Calculate contrast
lum_effective_dark = relative_luminance(effective_color_dark)
lum_dark_bg = relative_luminance(dark_bg)
ratio_dark = contrast_ratio(lum_effective_dark, lum_dark_bg)

print(f"Dark mode contrast ratio: {ratio_dark:.2f}:1")
print(f"WCAG AA normal text (4.5:1): {'PASS' if ratio_dark >= 4.5 else 'FAIL'}")

# Also check if text-slate-500 would be better (light mode)
slate_500 = (100, 116, 139)
effective_500 = apply_opacity(slate_500, (255, 255, 255), 0.6)
print(f"\nLight mode: Slate-500 at 60% opacity: rgb{effective_500}")
lum_500 = relative_luminance(effective_500)
lum_white = relative_luminance((255, 255, 255))
ratio_500 = contrast_ratio(lum_500, lum_white)
print(f"Slate-500 contrast ratio: {ratio_500:.2f}:1")
print(f"WCAG AA normal text (4.5:1): {'PASS' if ratio_500 >= 4.5 else 'FAIL'}")
EOF

Repository: CatholicOS/ontokit-web

Length of output: 333


Address WCAG AA contrast issue in light mode for dimmed labels.

The opacity-60 styling on labels with text-slate-700 results in a 3.36:1 contrast ratio in light mode, which fails the WCAG AA requirement of 4.5:1 for normal text (the labels use text-sm). Dark mode with text-slate-300 passes at 4.51:1, so the issue is light-mode only.

The suggested alternative of using text-slate-500 would worsen the issue to 2.32:1. Instead, consider:

  • Using text-slate-600 for better contrast while maintaining the dimmed visual
  • Increasing opacity (e.g., opacity-70 or opacity-75)
  • Applying opacity-60 only in dark mode where it still passes

This affects the labels for Repository owner, Repository name, Branch, and File path (lines 2886–2889, 2907–2910, 2942–2945, 2963–2966).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/projects/`[id]/settings/page.tsx around lines 2886 - 2889, The label
rendering using cn(...) with isWebhookDriven currently applies "opacity-60"
which drops the light-mode contrast below WCAG AA; update the class logic in the
label elements (the <label> usages that call cn with isWebhookDriven — e.g., the
Repository owner/name, Branch, and File path labels) so that when
isWebhookDriven is true you either use a higher-contrast text class (e.g.,
"text-slate-600") in light mode or increase opacity (e.g., "opacity-75"), or
apply "opacity-60" only for dark mode; implement this by changing the
conditional passed to cn(...) (reference the isWebhookDriven conditional in
page.tsx) to choose between "text-slate-600" or "opacity-75" for light mode and
keep "opacity-60" for dark mode via a dark: utility or two-branch conditional.

R-Hart80 and others added 4 commits May 23, 2026 16:37
When a project is webhook-driven, the four GitHub integration input
fields (repo owner, repo name, branch, file path) were already marked
readOnly and visually styled as disabled on the inputs themselves.
This commit extends the visual treatment to their labels by applying
opacity-60 when isWebhookDriven is true, and moves the explanatory
note ("Repository fields are managed by the GitHub integration.")
to appear after all four fields so it clearly covers the whole group.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mergePathsIntoTree now accepts a query string and marks any node
isSearchMatch when its label contains the query (case-insensitive),
not only the leaf IRIs returned by the backend. Plumbed searchQuery
from useTreeSearch through UseFilteredTreeOptions into the merge.

Four new tests cover: ancestor highlight, case-insensitivity, empty
query no-op, and non-matching ancestor stays unhighlighted. Fixes CatholicOS#209.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tholicOS#200)

Replace four direct setState calls inside useEffect bodies with lazy
initialisers and derived state:

- EntityTreeToolbar: useState lazy init reads localStorage instead of
  setting state in a mount effect; removes the useEffect entirely.
- app/auth/error: move auto-retry call into a setTimeout callback so it
  is asynchronous; removes the redundant setRetryCount that duplicated
  what retry() already does.
- app/settings: useState lazy init reads window.location.hash so the
  highlight value is available on the first render without a
  setState-in-effect.
- app/page: replace authDefaultApplied ref + useEffect with a derived
  filter (null-coalesce on userFilter) so authenticated users default to
  "mine" without an extra render cycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Missing dep caused stale tree highlights when the search query changed
but searchResults did not (e.g. same result set, different query string).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@R-Hart80 R-Hart80 force-pushed the fix/setState-in-effect-category-d branch from 157b859 to 4efc369 Compare May 23, 2026 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant