Skip to content

Enable react-next ESLint rules#43

Merged
ryota-murakami merged 3 commits into
mainfrom
codex/react-next-eslint-plugin
May 12, 2026
Merged

Enable react-next ESLint rules#43
ryota-murakami merged 3 commits into
mainfrom
codex/react-next-eslint-plugin

Conversation

@ryota-murakami
Copy link
Copy Markdown
Collaborator

@ryota-murakami ryota-murakami commented May 12, 2026

Summary

  • install @laststance/react-next-eslint-plugin and enable all plugin rules as errors
  • memoize React components and stabilize callback/value props surfaced by the new rules
  • remove the temporary stability marker escape hatch and use real memoization or explicit scoped ignores for legacy/generated surfaces

Validation

  • pnpm validate
  • git diff --check

Summary by CodeRabbit

  • Refactor

    • Wide memoization of UI components and migration of effects to a component-safe hook to reduce renders and improve stability
    • Several performance and lifecycle tweaks across pages, widgets, and desktop flows
  • New Features / Chores

    • Added small component hooks for reducer-style state and component-effect ergonomics
    • Updated linting config and added a new dev ESLint plugin
  • Tests

    • Test imports adjusted to ensure React types resolve consistently

Review Change Stack

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
corelive Ready Ready Preview, Comment May 12, 2026 2:03pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c07a450-b623-4b6f-a958-fd6c810ea682

📥 Commits

Reviewing files that changed from the base of the PR and between 3c63645 and a979688.

📒 Files selected for processing (2)
  • src/app/oauth/callback/page.tsx
  • src/components/ui/toggle-group.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/ui/toggle-group.tsx
  • src/app/oauth/callback/page.tsx

📝 Walkthrough

Walkthrough

Adds ESLint plugin and config, introduces useComponentEffect and useReducerState hooks, and refactors many components/pages/providers/UI primitives to use React.memo and the new hook; preserves existing behavior while standardizing side-effect handling.

Changes

App-wide memoization, custom hooks, and ESLint wiring

Layer / File(s) Summary
ESLint plugin and rules
eslint.config.js, package.json
Wire @laststance/react-next plugin and generate a rules map; add @laststance/react-next-eslint-plugin to devDependencies.
New hooks
src/hooks/useComponentEffect.ts, src/hooks/useReducerState.ts
Add useComponentEffect wrapper and useReducerState local reducer-style state hook.
Providers & infra
src/providers/*, src/lib/redux/providers.tsx, src/providers/QueryClientProvider.tsx, src/providers/ThemeProvider.tsx
Memoize provider components; move persistence and sign-out effects to useComponentEffect; memoize persist/options and themes arrays.
Electron & auth
src/components/electron/*, src/lib/orpc/electron-auth-provider.tsx
Memoize Electron UI components and provider; replace useEffect with useComponentEffect in OAuth/listener/sync logic; swap some useReducer → useReducerState.
Pages
src/app/**/page.tsx, OAuth callback/start/sso pages
Memoize page components and replace page effect hooks with useComponentEffect where applicable; maintain routing/auth flows.
Home & todos
src/app/(main)/home/_components/*
Memoize home components; replace cross-window and IO effects with useComponentEffect; introduce memoized callbacks for controlled open handlers.
Floating navigator & skill-tree
src/components/floating-navigator/*, src/app/(main)/skill-tree/*
Memoize navigator and skill-tree components; move effects to useComponentEffect; preserve DnD and sync behaviors.
UI primitives
src/components/ui/*
Large sweep converting UI primitives to React.memo and reordering className tokens; a few components wrap children differently while keeping props and behavior intact.
Charts & animations
src/components/ui/chart.tsx, src/components/animations/*, src/components/ui/calendar.tsx
Memoize chart/tooltip/legend components; add sanitization for chart CSS variables; move animation timers/effects to useComponentEffect/useMemo patterns.
Tests & small edits
src/app/**/**/*.test.tsx, src/types/react.d.ts, src/lib/utils.ts
Add type-only React imports in tests and types; small import and typing adjustments.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • laststance/corelive#40 — Touches many of the same home components updated here (CategoryFilterChips, YearInReviewModal).
  • laststance/corelive#38 — Modifies ContributionGraph and related heatmap/day-detail flows updated in this PR.
  • laststance/corelive#21 — Introduced skill-tree components that are memoized/refactored in this PR.

Poem

A rabbit taps keys with a caffeinated grin,
Wrapping each component in a memoed twin.
Effects hop to a custom burrow to run,
Hooks and rules align beneath the sun.
ESLint nods, the codebase hums—refactor done! 🥕

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/react-next-eslint-plugin

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 12, 2026

Codecov Report

❌ Patch coverage is 86.48649% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.06%. Comparing base (3fbe8fa) to head (a979688).

Files with missing lines Patch % Lines
src/components/ui/card.tsx 64.28% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #43      +/-   ##
==========================================
+ Coverage   75.53%   76.06%   +0.53%     
==========================================
  Files          33       34       +1     
  Lines         846      865      +19     
  Branches      217      216       -1     
==========================================
+ Hits          639      658      +19     
  Misses        189      189              
  Partials       18       18              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
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: 8

Caution

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

⚠️ Outside diff range comments (6)
src/components/ui/toggle-group.tsx (1)

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

Preserve ref support while memoizing these primitives

These wrappers are memoized but don't expose ref to the underlying Radix root/item elements, which breaks focus-management and imperative integrations. Additionally, the context value at line 36 is recreated as a new object literal on each render, negating the memoization benefits for context consumers.

Suggested pattern (`memo(forwardRef(...))`)
-const ToggleGroup = React.memo(function ToggleGroup(props) { ... })
+const ToggleGroup = React.memo(
+  React.forwardRef<
+    React.ElementRef<typeof ToggleGroupPrimitive.Root>,
+    React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
+      VariantProps<typeof toggleVariants>
+  >(function ToggleGroup(props, ref) {
+    return <ToggleGroupPrimitive.Root ref={ref} ... />
+  }),
+)

-const ToggleGroupItem = React.memo(function ToggleGroupItem(props) { ... })
+const ToggleGroupItem = React.memo(
+  React.forwardRef<
+    React.ElementRef<typeof ToggleGroupPrimitive.Item>,
+    React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
+      VariantProps<typeof toggleVariants>
+  >(function ToggleGroupItem(props, ref) {
+    return <ToggleGroupPrimitive.Item ref={ref} ... />
+  }),
+)

For the context value, memoize it with useMemo to preserve object identity across renders.

Also applies to: 43-50

🤖 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 `@src/components/ui/toggle-group.tsx` around lines 17 - 24, The memoized
wrapper components (e.g., ToggleGroup and the related ToggleItem wrappers
wrapping ToggleGroupPrimitive.Root/Item) currently drop refs and recreate
context on every render; change their definitions to use memo(forwardRef(...))
so the forwarded ref is passed through to the underlying Radix primitives
(ensure the forwarded ref is applied to ToggleGroupPrimitive.Root /
ToggleGroupPrimitive.Item), and memoize the context value with useMemo (based on
the props/state that determine it) so the object identity is stable across
renders; update the component signatures (ToggleGroup, ToggleItem) to accept ref
and forward it, and wrap the final export with React.memo(forwardRef(...)) while
using useMemo for the context value creation.
src/components/ui/chart.tsx (1)

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

Escape or validate values before writing raw CSS into the <style> tag.

Lines 99-115 interpolate caller-supplied id, config keys, and color strings directly into dangerouslySetInnerHTML. That makes this component an HTML/CSS injection sink, and even benign id values with spaces/quotes will break the [data-chart=${id}] selector.

🤖 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 `@src/components/ui/chart.tsx` around lines 98 - 115, The inline CSS builder is
injecting unvalidated values (id, theme prefixes, config keys and color strings)
into dangerouslySetInnerHTML, making it vulnerable to CSS/HTML injection and
breaking selectors; fix by validating/escaping every dynamic piece before
interpolation: call CSS.escape on id and any selector fragments (e.g., theme
prefix keys from THEMES and any config keys used in selectors), and validate
colorConfig values with a strict color whitelist/regex (allow only safe
hex/rgb/rgba/hsl formats) or normalize them through a sanitizer function before
emitting; update the code that constructs the style string (the block using
THEMES, id, colorConfig and dangerouslySetInnerHTML) to use these
escaping/validation helpers so no raw caller-supplied value is written into the
style tag.
src/components/animations/ConfettiAnimation.tsx (1)

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

duration and onComplete become stale after initial mount.

The timer store is created once using useRef and never rebuilt, so if the component receives new duration or onComplete props after the initial render, those values won't be used. The next trigger will call start() with the original duration and callback captured in the store's closure.

Replace useRef with useMemo and include duration and onComplete in the dependency array to ensure the store is recreated whenever these props change:

Suggested fix
-  const timerStoreRef = useRef<ReturnType<typeof createTimerStore> | null>(null)
-  if (!timerStoreRef.current) {
-    timerStoreRef.current = createTimerStore(duration, onComplete)
-  }
+  const timerStore = useMemo(
+    () => createTimerStore(duration, onComplete),
+    [duration, onComplete],
+  )

   const isActive = useSyncExternalStore(
-    timerStoreRef.current.subscribe,
-    timerStoreRef.current.getSnapshot,
-    timerStoreRef.current.getServerSnapshot,
+    timerStore.subscribe,
+    timerStore.getSnapshot,
+    timerStore.getServerSnapshot,
   )

   // ... later ...
-    timerStoreRef.current.start()
+    timerStore.start()
🤖 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 `@src/components/animations/ConfettiAnimation.tsx` around lines 106 - 109, The
timer store is created once with timerStoreRef and captures initial duration and
onComplete, so updates to those props become stale; replace the useRef-based
creation with useMemo to recreate the store whenever duration or onComplete
change (i.e. create the store via useMemo(() => createTimerStore(duration,
onComplete), [duration, onComplete])) so that the createTimerStore closure
always sees current duration and onComplete and subsequent calls like start()
use latest values.
src/app/(main)/home/_components/ContributionGraph.tsx (1)

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

Width observation can be skipped after the loading-first render

When the initial render hits the loading branch, elementRef.current is null and this effect exits. With deps set to [elementRef], it may not rerun after the container mounts, leaving containerWidth stuck null and the heatmap at fallback sizing.

💡 Suggested fix
- const containerWidth = useObservedElementWidth(containerRef)
+ const containerWidth = useObservedElementWidth(containerRef, !isLoading)
-function useObservedElementWidth<T extends HTMLElement>(
-  elementRef: React.RefObject<T | null>,
-): number | null {
+function useObservedElementWidth<T extends HTMLElement>(
+  elementRef: React.RefObject<T | null>,
+  enabled: boolean,
+): number | null {
   const [elementWidth, setElementWidth] = useState<number | null>(null)

   useComponentEffect(() => {
+    if (!enabled) return
     const element = elementRef.current

     if (!element) {
       return
     }
@@
-  }, [elementRef])
+  }, [enabled, elementRef])

   return elementWidth
 }
🤖 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 `@src/app/`(main)/home/_components/ContributionGraph.tsx around lines 374 -
401, The effect in useComponentEffect inside ContributionGraph exits early when
elementRef.current is null on the initial loading render and never re-runs
because the deps are [elementRef]; change the dependency to watch the actual ref
value so the effect re-runs when the container mounts (e.g., use
[elementRef.current] or [elementRef.current ?? null]) and keep the same logic
(updateWidth, ResizeObserver fallback, cleanup) so containerWidth is set after
the element becomes available; update references to element inside the effect to
use a local const element = elementRef.current to avoid stale reads.
src/app/oauth/callback/page.tsx (1)

39-108: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add cleanup for redirect/status timers

The two setTimeout callbacks are never cleared. If the component unmounts/remounts quickly, stale callbacks can still fire and mutate UI flow late.

Suggested cleanup pattern
   useComponentEffect(() => {
+    let redirectTimer: number | undefined
+    let successTimer: number | undefined
+
     const state = searchParams.get('state')
@@
-        setTimeout(() => {
+        redirectTimer = window.setTimeout(() => {
           window.location.href = deepLink
         }, 100)
@@
-        setTimeout(() => {
+        successTimer = window.setTimeout(() => {
           setStatus('success')
         }, 2000)
@@
     void createTokenAndRedirect()
+    return () => {
+      if (redirectTimer !== undefined) window.clearTimeout(redirectTimer)
+      if (successTimer !== undefined) window.clearTimeout(successTimer)
+    }
   }, [searchParams])
🤖 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 `@src/app/oauth/callback/page.tsx` around lines 39 - 108, The two setTimeout
calls inside createTokenAndRedirect (in the useComponentEffect callback) create
timers that are never cleared and can fire after the component unmounts; modify
the effect to capture the timeout IDs (e.g., redirectTimeoutId and
successTimeoutId) and return a cleanup function that calls clearTimeout on them
(and optionally set a mounted flag to avoid calling setStatus after unmount).
Update createTokenAndRedirect / useComponentEffect to store and clear those
timers so stale callbacks cannot mutate state after unmounting.
src/components/animations/LevelUpAnimation.tsx (1)

67-70: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

onComplete can become stale after prop updates

Both components create the visibility store once and capture the initial onComplete. If onComplete changes later, the timeout still calls the old callback.

Suggested fix
 export const LevelUpAnimation = React.memo(function LevelUpAnimation({
   level,
   show,
   onComplete,
   className,
 }: LevelUpAnimationProps) {
+  const onCompleteRef = useRef(onComplete)
+  onCompleteRef.current = onComplete
   const prevShowRef = useRef(false)

   // Create stable visibility store
   const storeRef = useRef<ReturnType<typeof createVisibilityStore> | null>(null)
   if (!storeRef.current) {
-    storeRef.current = createVisibilityStore(4000, onComplete)
+    storeRef.current = createVisibilityStore(4000, () =>
+      onCompleteRef.current?.(),
+    )
   }
   function MilestoneLevelUpAnimation({
     level,
     show,
     milestone,
     reward,
     onComplete,
     className,
   }: LevelUpAnimationProps & {
     milestone: string
     reward?: string
   }) {
+    const onCompleteRef = useRef(onComplete)
+    onCompleteRef.current = onComplete
     const prevShowRef = useRef(false)

     // Create stable visibility store with longer duration for milestones
     const storeRef = useRef<ReturnType<typeof createVisibilityStore> | null>(
       null,
     )
     if (!storeRef.current) {
-      storeRef.current = createVisibilityStore(5000, onComplete)
+      storeRef.current = createVisibilityStore(5000, () =>
+        onCompleteRef.current?.(),
+      )
     }

Also applies to: 138-143

🤖 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 `@src/components/animations/LevelUpAnimation.tsx` around lines 67 - 70, The
visibility store is created once and closes over the initial onComplete, so
later prop updates are ignored; fix by making the store call a stable wrapper
that reads a mutable ref to the latest onComplete: create a ref like
latestOnCompleteRef.current = onComplete in a useEffect, and when creating the
store (storeRef = createVisibilityStore(...)) pass a stable function (e.g. () =>
latestOnCompleteRef.current?.()) instead of onComplete; update the same pattern
for the second occurrence (lines ~138-143) so the store always invokes the
current prop callback without recreating the store on every render.
🧹 Nitpick comments (4)
src/components/ui/toggle-group.tsx (1)

36-38: ⚡ Quick win

Stabilize context value identity

At Line 36, value={{ variant, size }} creates a new object each render, so all ToggleGroupItem consumers re-render even when values are unchanged.

Suggested fix
+  const contextValue = React.useMemo(() => ({ variant, size }), [variant, size])

-      <ToggleGroupContext value={{ variant, size }}>
+      <ToggleGroupContext value={contextValue}>
         {children}
       </ToggleGroupContext>
🤖 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 `@src/components/ui/toggle-group.tsx` around lines 36 - 38, The context value
passed to ToggleGroupContext is recreated each render (value={{ variant, size
}}), causing unnecessary re-renders of consumers like ToggleGroupItem; fix by
memoizing the context value in the ToggleGroup component (useMemo or a stable
ref) e.g. derive a stable object from variant and size and pass that memoized
object to <ToggleGroupContext value={...}>, ensuring the identity only changes
when variant or size change.
src/app/(main)/skill-tree/SkillTreeView.tsx (1)

381-381: ⚡ Quick win

Use a stable onOpenChange reference for TaskPoolDrawer.

The TaskPoolDrawer component is memoized but line 381 creates a new callback function on every render, defeating the memoization optimization. Pass the setter function directly instead.

♻️ Suggested change
-            onOpenChange={(open: boolean) => setDrawerOpen(open)}
+            onOpenChange={setDrawerOpen}
🤖 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 `@src/app/`(main)/skill-tree/SkillTreeView.tsx at line 381, TaskPoolDrawer is
memoized but you're creating a new arrow callback each render with
onOpenChange={(open: boolean) => setDrawerOpen(open)} which breaks the
memoization; replace that inline callback by passing the stable setter directly
(onOpenChange={setDrawerOpen}) so TaskPoolDrawer receives a stable reference and
memoization can work as intended.
src/app/(main)/home/_components/AddTodoForm.tsx (1)

82-85: ⚡ Quick win

Use one stable onOpenChange handler.

Two identical inline handlers at lines 84 and 117 are recreated on every render. Since the component is already wrapped in React.memo(), passing fresh callback references on each render breaks memoization benefits. Extract a single useCallback-memoized handler to keep props stable.

♻️ Suggested refactor
-import React, { useState } from 'react'
+import React, { useCallback, useState } from 'react'
...
   const [isNotesOpen, setIsNotesOpen] = useState(false)
+  const handleNotesOpenChange = useCallback((open: boolean) => {
+    setIsNotesOpen(open)
+  }, [])
...
-              <Collapsible
-                open={isNotesOpen}
-                onOpenChange={(open: boolean) => setIsNotesOpen(open)}
-              >
+              <Collapsible open={isNotesOpen} onOpenChange={handleNotesOpenChange}>
...
-            <Collapsible
-              open={isNotesOpen}
-              onOpenChange={(open: boolean) => setIsNotesOpen(open)}
-            >
+            <Collapsible open={isNotesOpen} onOpenChange={handleNotesOpenChange}>
🤖 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 `@src/app/`(main)/home/_components/AddTodoForm.tsx around lines 82 - 85, The
inline onOpenChange handlers in AddTodoForm.tsx recreate new functions each
render and break React.memo benefits; extract a single memoized handler using
useCallback (e.g., const handleNotesOpenChange = useCallback((open: boolean) =>
setIsNotesOpen(open), [setIsNotesOpen])) and replace both inline onOpenChange
props on the Collapsible(s) with this stable handleNotesOpenChange so the prop
reference remains stable across renders.
src/app/(main)/home/_components/TodoItem.tsx (1)

118-121: ⚡ Quick win

Stabilize onOpenChange callback identity.

At lines 120 and 153, identical inline handlers are recreated every render, undermining memoization benefits on the Collapsible component. Extract to a single useCallback handler with an empty dependency array.

♻️ Suggested refactor
-import React, { useState } from 'react'
+import React, { useCallback, useState } from 'react'
...
   const [isNotesOpen, setIsNotesOpen] = useState(false)
+  const handleNotesOpenChange = useCallback((open: boolean) => {
+    setIsNotesOpen(open)
+  }, [])
...
-            <Collapsible
-              open={isNotesOpen}
-              onOpenChange={(open: boolean) => setIsNotesOpen(open)}
-            >
+            <Collapsible open={isNotesOpen} onOpenChange={handleNotesOpenChange}>
...
-        <Collapsible
-          open={isNotesOpen}
-          onOpenChange={(open: boolean) => setIsNotesOpen(open)}
-        >
+        <Collapsible open={isNotesOpen} onOpenChange={handleNotesOpenChange}>
🤖 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 `@src/app/`(main)/home/_components/TodoItem.tsx around lines 118 - 121, The
inline onOpenChange handlers passed to the Collapsible component (currently
(open: boolean) => setIsNotesOpen(open)) are recreated each render and break
memoization; extract a stable handler using React's useCallback (e.g., const
handleNotesOpenChange = useCallback((open: boolean) => setIsNotesOpen(open),
[])) and replace the inline prop on Collapsible with handleNotesOpenChange so
its identity remains stable; update both occurrences (lines using
isNotesOpen/setIsNotesOpen) to use this single callback.
🤖 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 `@src/app/`(main)/home/_components/TodoList.tsx:
- Around line 226-228: The effect that sets localPendingTodos uses
pendingTodosFromQuery (which itself relies on pendingData and categoryMap via
mapTodos) but only lists pendingData in the dependency array; update the
useComponentEffect for setLocalPendingTodos so it re-runs when the derived todos
change by adding pendingTodosFromQuery (or categoryMap) to the dependency list
instead of only pendingData, ensuring localPendingTodos picks up
categoryName/categoryColor updates.

In `@src/components/animations/AchievementAnimation.tsx`:
- Around line 119-132: handleComplete mutates shouldReset inside the
setAchievements updater so setCurrentIndex returns before that mutation, causing
out-of-bounds currentIndex; fix by reading the current state synchronously
before scheduling updates (use current values of currentIndex and achievements),
then call setAchievements and setCurrentIndex based on that snapshot (e.g.,
compute isLast = currentIndex >= achievements.length - 1, then
setAchievements(isLast ? [] : prev => prev) and setCurrentIndex(isLast ? 0 :
currentIndex + 1)); also add currentIndex and achievements (and the setters) to
the handleComplete dependency array so the hook uses up-to-date state.

In `@src/components/electron/ConfigurationSettings.tsx`:
- Around line 160-176: The updateConfig function allows NaN to be written into
config; add a guard before assigning to current[lastKey] that detects numeric
NaN values and prevents them from being written (e.g., if typeof value ===
'number' && isNaN(value') then skip the assignment and return or leave existing
value intact). Modify the code inside updateConfig (referencing updateConfig,
current, lastKey, and config) so invalid numeric inputs are rejected at the
write boundary instead of storing NaN in state.

In `@src/components/ui/chart.tsx`:
- Line 68: ChartContext is being passed a new object literal each render in
ChartContainer which forces all useChart() consumers to rerender; fix this by
memoizing the context value inside ChartContainer (useMemo) so the same
reference is reused when config is unchanged—compute a memoized value (dependent
on config) and pass that to <ChartContext value={...}> instead of creating `{
config }` inline.

In `@src/components/ui/menubar.tsx`:
- Around line 81-83: The menubar content className string is missing the
close-state animation token; in the MenubarContent JSX where you build className
with cn(...) (the block starting with 'data-[state=open]:animate-in ...'),
insert the token data-[state=closed]:animate-out immediately after
data-[state=open]:animate-in so the closed-state animation will trigger (match
the pattern used in other components like Popover/Select/DropdownMenu).

In `@src/hooks/useReducerState.ts`:
- Around line 21-26: The dispatch returned from useReducerState is re-created
whenever the reducer reference changes because it's memoized with [reducer]; to
honor the hook's docstring promise of a stable dispatch function, rewrite
dispatch to use a ref-backed stable callback: store a ref to the current reducer
(update it in an effect) and create a single dispatch function (no reducer
dependency) that calls setState(prev => reducerRef.current(prev, action));
update references to reducerRef and keep setState usage intact so the returned
dispatch identity never changes even if the reducer prop changes.

In `@src/lib/redux/providers.tsx`:
- Line 17: The usage example incorrectly imports memo from the local module;
update the example import so that ReduxProvider still comes from
'@/lib/redux/providers' but memo is imported from 'react' (i.e., import { memo }
from 'react') to avoid a broken import — adjust the example line that currently
reads "import { memo, ReduxProvider } from '@/lib/redux/providers'" to separate
memo's import from the local ReduxProvider export.

In `@src/types/react.d.ts`:
- Around line 1-2: Remove the conflicting default type import and keep the
namespace import: delete the line `import type React from 'react'` and retain
`import type * as React from 'react'`; ensure usages like `React.Dispatch`
continue to use the namespace import and no other default `React` type import
remains in this declaration file.

---

Outside diff comments:
In `@src/app/`(main)/home/_components/ContributionGraph.tsx:
- Around line 374-401: The effect in useComponentEffect inside ContributionGraph
exits early when elementRef.current is null on the initial loading render and
never re-runs because the deps are [elementRef]; change the dependency to watch
the actual ref value so the effect re-runs when the container mounts (e.g., use
[elementRef.current] or [elementRef.current ?? null]) and keep the same logic
(updateWidth, ResizeObserver fallback, cleanup) so containerWidth is set after
the element becomes available; update references to element inside the effect to
use a local const element = elementRef.current to avoid stale reads.

In `@src/app/oauth/callback/page.tsx`:
- Around line 39-108: The two setTimeout calls inside createTokenAndRedirect (in
the useComponentEffect callback) create timers that are never cleared and can
fire after the component unmounts; modify the effect to capture the timeout IDs
(e.g., redirectTimeoutId and successTimeoutId) and return a cleanup function
that calls clearTimeout on them (and optionally set a mounted flag to avoid
calling setStatus after unmount). Update createTokenAndRedirect /
useComponentEffect to store and clear those timers so stale callbacks cannot
mutate state after unmounting.

In `@src/components/animations/ConfettiAnimation.tsx`:
- Around line 106-109: The timer store is created once with timerStoreRef and
captures initial duration and onComplete, so updates to those props become
stale; replace the useRef-based creation with useMemo to recreate the store
whenever duration or onComplete change (i.e. create the store via useMemo(() =>
createTimerStore(duration, onComplete), [duration, onComplete])) so that the
createTimerStore closure always sees current duration and onComplete and
subsequent calls like start() use latest values.

In `@src/components/animations/LevelUpAnimation.tsx`:
- Around line 67-70: The visibility store is created once and closes over the
initial onComplete, so later prop updates are ignored; fix by making the store
call a stable wrapper that reads a mutable ref to the latest onComplete: create
a ref like latestOnCompleteRef.current = onComplete in a useEffect, and when
creating the store (storeRef = createVisibilityStore(...)) pass a stable
function (e.g. () => latestOnCompleteRef.current?.()) instead of onComplete;
update the same pattern for the second occurrence (lines ~138-143) so the store
always invokes the current prop callback without recreating the store on every
render.

In `@src/components/ui/chart.tsx`:
- Around line 98-115: The inline CSS builder is injecting unvalidated values
(id, theme prefixes, config keys and color strings) into
dangerouslySetInnerHTML, making it vulnerable to CSS/HTML injection and breaking
selectors; fix by validating/escaping every dynamic piece before interpolation:
call CSS.escape on id and any selector fragments (e.g., theme prefix keys from
THEMES and any config keys used in selectors), and validate colorConfig values
with a strict color whitelist/regex (allow only safe hex/rgb/rgba/hsl formats)
or normalize them through a sanitizer function before emitting; update the code
that constructs the style string (the block using THEMES, id, colorConfig and
dangerouslySetInnerHTML) to use these escaping/validation helpers so no raw
caller-supplied value is written into the style tag.

In `@src/components/ui/toggle-group.tsx`:
- Around line 17-24: The memoized wrapper components (e.g., ToggleGroup and the
related ToggleItem wrappers wrapping ToggleGroupPrimitive.Root/Item) currently
drop refs and recreate context on every render; change their definitions to use
memo(forwardRef(...)) so the forwarded ref is passed through to the underlying
Radix primitives (ensure the forwarded ref is applied to
ToggleGroupPrimitive.Root / ToggleGroupPrimitive.Item), and memoize the context
value with useMemo (based on the props/state that determine it) so the object
identity is stable across renders; update the component signatures (ToggleGroup,
ToggleItem) to accept ref and forward it, and wrap the final export with
React.memo(forwardRef(...)) while using useMemo for the context value creation.

---

Nitpick comments:
In `@src/app/`(main)/home/_components/AddTodoForm.tsx:
- Around line 82-85: The inline onOpenChange handlers in AddTodoForm.tsx
recreate new functions each render and break React.memo benefits; extract a
single memoized handler using useCallback (e.g., const handleNotesOpenChange =
useCallback((open: boolean) => setIsNotesOpen(open), [setIsNotesOpen])) and
replace both inline onOpenChange props on the Collapsible(s) with this stable
handleNotesOpenChange so the prop reference remains stable across renders.

In `@src/app/`(main)/home/_components/TodoItem.tsx:
- Around line 118-121: The inline onOpenChange handlers passed to the
Collapsible component (currently (open: boolean) => setIsNotesOpen(open)) are
recreated each render and break memoization; extract a stable handler using
React's useCallback (e.g., const handleNotesOpenChange = useCallback((open:
boolean) => setIsNotesOpen(open), [])) and replace the inline prop on
Collapsible with handleNotesOpenChange so its identity remains stable; update
both occurrences (lines using isNotesOpen/setIsNotesOpen) to use this single
callback.

In `@src/app/`(main)/skill-tree/SkillTreeView.tsx:
- Line 381: TaskPoolDrawer is memoized but you're creating a new arrow callback
each render with onOpenChange={(open: boolean) => setDrawerOpen(open)} which
breaks the memoization; replace that inline callback by passing the stable
setter directly (onOpenChange={setDrawerOpen}) so TaskPoolDrawer receives a
stable reference and memoization can work as intended.

In `@src/components/ui/toggle-group.tsx`:
- Around line 36-38: The context value passed to ToggleGroupContext is recreated
each render (value={{ variant, size }}), causing unnecessary re-renders of
consumers like ToggleGroupItem; fix by memoizing the context value in the
ToggleGroup component (useMemo or a stable ref) e.g. derive a stable object from
variant and size and pass that memoized object to <ToggleGroupContext
value={...}>, ensuring the identity only changes when variant or size change.
🪄 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 Plus

Run ID: ff95a3e0-6b0c-4801-bb1a-9abc45f509be

📥 Commits

Reviewing files that changed from the base of the PR and between 3fbe8fa and 248a770.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (117)
  • eslint.config.js
  • package.json
  • src/app/(main)/home/_components/AddTodoForm.tsx
  • src/app/(main)/home/_components/Category.tsx
  • src/app/(main)/home/_components/CategoryFilterChips.tsx
  • src/app/(main)/home/_components/CategoryManageDialog.tsx
  • src/app/(main)/home/_components/CompletedTodos.tsx
  • src/app/(main)/home/_components/ContributionGraph.tsx
  • src/app/(main)/home/_components/DayDetailDialog.tsx
  • src/app/(main)/home/_components/LogoutButton.tsx
  • src/app/(main)/home/_components/SortableTodoItem.tsx
  • src/app/(main)/home/_components/SundayDigestCard.tsx
  • src/app/(main)/home/_components/TodoItem.tsx
  • src/app/(main)/home/_components/TodoList.tsx
  • src/app/(main)/home/_components/WeeklySummaryCard.tsx
  • src/app/(main)/home/_components/YearInReviewModal.tsx
  • src/app/(main)/home/page.tsx
  • src/app/(main)/layout.tsx
  • src/app/(main)/skill-tree/SkillTreeView.tsx
  • src/app/(main)/skill-tree/components/ConstellationCanvas.tsx
  • src/app/(main)/skill-tree/components/DragOverlayCard.tsx
  • src/app/(main)/skill-tree/components/NodePopover.tsx
  • src/app/(main)/skill-tree/components/SkillNodeCircle.test.tsx
  • src/app/(main)/skill-tree/components/SkillNodeCircle.tsx
  • src/app/(main)/skill-tree/components/TaskPoolCard.test.tsx
  • src/app/(main)/skill-tree/components/TaskPoolCard.tsx
  • src/app/(main)/skill-tree/components/TaskPoolDrawer.stories.tsx
  • src/app/(main)/skill-tree/components/TaskPoolDrawer.tsx
  • src/app/(main)/skill-tree/components/XpBadge.tsx
  • src/app/(main)/skill-tree/page.tsx
  • src/app/braindump/page.tsx
  • src/app/floating-navigator/page.tsx
  • src/app/layout.tsx
  • src/app/login/[[...login]]/page.tsx
  • src/app/oauth/callback/page.tsx
  • src/app/oauth/sso-callback/page.tsx
  • src/app/oauth/start/page.tsx
  • src/app/page.tsx
  • src/app/settings/page.tsx
  • src/app/sign-up/[[...sign-up]]/page.tsx
  • src/components/AppSidebar.tsx
  • src/components/ThemeSelector.tsx
  • src/components/ThemeSelectorMenuItem.tsx
  • src/components/animations/AchievementAnimation.tsx
  • src/components/animations/ConfettiAnimation.tsx
  • src/components/animations/LevelUpAnimation.tsx
  • src/components/auth/ElectronLoginForm.tsx
  • src/components/auth/ElectronOAuthButtons.tsx
  • src/components/auth/ElectronSignUpForm.tsx
  • src/components/braindump/BrainDumpEditor.tsx
  • src/components/electron/BrainDumpSettings.tsx
  • src/components/electron/ConfigurationSettings.tsx
  • src/components/electron/ElectronSettingsPage.tsx
  • src/components/electron/ElectronStartupSync.tsx
  • src/components/electron/FloatingWindowSettings.tsx
  • src/components/electron/NotificationSettings.tsx
  • src/components/electron/ShortcutSettings.tsx
  • src/components/electron/WindowStateSettings.tsx
  • src/components/floating-navigator/FloatingCategoryManager.tsx
  • src/components/floating-navigator/FloatingNavigator.tsx
  • src/components/floating-navigator/FloatingNavigatorContainer.tsx
  • src/components/floating-navigator/SortableFloatingTodoItem.tsx
  • src/components/floating-navigator/useFloatingNavigatorMenuActions.ts
  • src/components/ui/accordion.tsx
  • src/components/ui/alert-dialog.tsx
  • src/components/ui/alert.tsx
  • src/components/ui/aspect-ratio.tsx
  • src/components/ui/avatar.tsx
  • src/components/ui/badge.tsx
  • src/components/ui/breadcrumb.tsx
  • src/components/ui/button.tsx
  • src/components/ui/calendar.tsx
  • src/components/ui/card.tsx
  • src/components/ui/carousel.tsx
  • src/components/ui/chart.tsx
  • src/components/ui/checkbox.tsx
  • src/components/ui/collapsible.tsx
  • src/components/ui/command.tsx
  • src/components/ui/context-menu.tsx
  • src/components/ui/dialog.tsx
  • src/components/ui/drawer.tsx
  • src/components/ui/dropdown-menu.tsx
  • src/components/ui/form.tsx
  • src/components/ui/hover-card.tsx
  • src/components/ui/input-otp.tsx
  • src/components/ui/input.tsx
  • src/components/ui/label.tsx
  • src/components/ui/menubar.tsx
  • src/components/ui/navigation-menu.tsx
  • src/components/ui/pagination.tsx
  • src/components/ui/popover.tsx
  • src/components/ui/progress.tsx
  • src/components/ui/radio-group.tsx
  • src/components/ui/resizable.tsx
  • src/components/ui/scroll-area.tsx
  • src/components/ui/select.tsx
  • src/components/ui/separator.tsx
  • src/components/ui/sheet.tsx
  • src/components/ui/sidebar.tsx
  • src/components/ui/skeleton.tsx
  • src/components/ui/slider.tsx
  • src/components/ui/sonner.tsx
  • src/components/ui/switch.tsx
  • src/components/ui/table.tsx
  • src/components/ui/tabs.tsx
  • src/components/ui/textarea.tsx
  • src/components/ui/toggle-group.tsx
  • src/components/ui/toggle.tsx
  • src/components/ui/tooltip.tsx
  • src/hooks/useComponentEffect.ts
  • src/hooks/useReducerState.ts
  • src/lib/orpc/electron-auth-provider.tsx
  • src/lib/redux/providers.tsx
  • src/lib/utils.ts
  • src/providers/QueryClientProvider.tsx
  • src/providers/ThemeProvider.tsx
  • src/types/react.d.ts

Comment thread src/app/(main)/home/_components/TodoList.tsx Outdated
Comment thread src/components/animations/AchievementAnimation.tsx Outdated
Comment thread src/components/electron/ConfigurationSettings.tsx
Comment thread src/components/ui/chart.tsx Outdated
Comment thread src/components/ui/menubar.tsx
Comment thread src/hooks/useReducerState.ts Outdated
Comment thread src/lib/redux/providers.tsx Outdated
Comment thread src/types/react.d.ts Outdated
Copy link
Copy Markdown
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 (1)
src/components/animations/ConfettiAnimation.tsx (1)

121-125: ⚡ Quick win

Consider moving the side effect to an effect hook.

Calling timerStore.start() during render triggers listener notifications synchronously, which can cause React to warn about updates during render. With React 19's concurrent features, side effects during render can lead to unpredictable behavior.

Moving this to an effect would be safer:

♻️ Suggested refactor
-  // Detect trigger rising edge during render (not in effect)
-  if (trigger && !prevTriggerRef.current && !isActive) {
-    particleSeedRef.current = Date.now()
-    timerStore.start()
-  }
-  prevTriggerRef.current = trigger
+  // Detect trigger rising edge and start animation
+  useEffect(() => {
+    if (trigger && !prevTriggerRef.current && !isActive) {
+      particleSeedRef.current = Date.now()
+      timerStore.start()
+    }
+    prevTriggerRef.current = trigger
+  }, [trigger, isActive, timerStore])
🤖 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 `@src/components/animations/ConfettiAnimation.tsx` around lines 121 - 125, The
render currently calls timerStore.start() when (trigger &&
!prevTriggerRef.current && !isActive), causing a side effect during render; move
this logic into a useEffect that depends on trigger and isActive (and refs as
needed) so the timerStore.start() call runs after commit; inside the effect,
when trigger becomes true and prevTriggerRef.current is false and !isActive, set
particleSeedRef.current = Date.now(), call timerStore.start(), and then update
prevTriggerRef.current = trigger (or update prevTriggerRef.current in a separate
effect) to preserve the same behavior without causing updates during render.
🤖 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 `@src/app/oauth/callback/page.tsx`:
- Around line 39-45: The effect in useComponentEffect is re-running because
searchParams (from useSearchParams) has an unstable identity; extract the
primitives by moving const state = searchParams.get('state'), const error =
searchParams.get('error') and const errorDescription =
searchParams.get('error_description') to component scope (outside
useComponentEffect) and then use those primitive variables in the effect's
dependency list instead of searchParams; update useComponentEffect to depend
only on state, error, and errorDescription so the POST to
/api/oauth/create-signin-token (and related redirect/timer logic in
useComponentEffect) runs only when the actual URL params change.

In `@src/components/ui/toggle-group.tsx`:
- Around line 58-70: The ToggleGroup context default is set to a truthy
'default' which prevents item-level props from ever being used; change the
ToggleGroupContext default values to undefined and update usages in
ToggleGroupItem to use optional chaining and nullish coalescing so item props
can fall back correctly (e.g. replace context.variant || variant and
context.size || size with context?.variant ?? variant and context?.size ?? size)
for all places including data-variant, data-size, and the call to toggleVariants
inside ToggleGroupPrimitive.Item.

---

Nitpick comments:
In `@src/components/animations/ConfettiAnimation.tsx`:
- Around line 121-125: The render currently calls timerStore.start() when
(trigger && !prevTriggerRef.current && !isActive), causing a side effect during
render; move this logic into a useEffect that depends on trigger and isActive
(and refs as needed) so the timerStore.start() call runs after commit; inside
the effect, when trigger becomes true and prevTriggerRef.current is false and
!isActive, set particleSeedRef.current = Date.now(), call timerStore.start(),
and then update prevTriggerRef.current = trigger (or update
prevTriggerRef.current in a separate effect) to preserve the same behavior
without causing updates during render.
🪄 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 Plus

Run ID: af666bba-f65d-4253-90bb-23a536111674

📥 Commits

Reviewing files that changed from the base of the PR and between 248a770 and 3c63645.

📒 Files selected for processing (16)
  • src/app/(main)/home/_components/AddTodoForm.tsx
  • src/app/(main)/home/_components/ContributionGraph.tsx
  • src/app/(main)/home/_components/TodoItem.tsx
  • src/app/(main)/home/_components/TodoList.tsx
  • src/app/(main)/skill-tree/SkillTreeView.tsx
  • src/app/oauth/callback/page.tsx
  • src/components/animations/AchievementAnimation.tsx
  • src/components/animations/ConfettiAnimation.tsx
  • src/components/animations/LevelUpAnimation.tsx
  • src/components/electron/ConfigurationSettings.tsx
  • src/components/ui/chart.tsx
  • src/components/ui/menubar.tsx
  • src/components/ui/toggle-group.tsx
  • src/hooks/useReducerState.ts
  • src/lib/redux/providers.tsx
  • src/types/react.d.ts
✅ Files skipped from review due to trivial changes (1)
  • src/types/react.d.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/components/animations/AchievementAnimation.tsx
  • src/components/electron/ConfigurationSettings.tsx
  • src/app/(main)/home/_components/TodoList.tsx
  • src/app/(main)/home/_components/TodoItem.tsx
  • src/components/animations/LevelUpAnimation.tsx
  • src/lib/redux/providers.tsx
  • src/app/(main)/home/_components/ContributionGraph.tsx
  • src/components/ui/menubar.tsx

Comment thread src/app/oauth/callback/page.tsx Outdated
Comment thread src/components/ui/toggle-group.tsx
@ryota-murakami ryota-murakami merged commit 8637432 into main May 12, 2026
22 of 23 checks passed
@ryota-murakami ryota-murakami deleted the codex/react-next-eslint-plugin branch May 12, 2026 14:27
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.

2 participants