Skip to content

refactor: decompose NoteModal + extract useEditableUrl hook (#151, #152)#158

Merged
ryota-murakami merged 3 commits intomainfrom
feat/bulk-issues-20260226
Feb 25, 2026
Merged

refactor: decompose NoteModal + extract useEditableUrl hook (#151, #152)#158
ryota-murakami merged 3 commits intomainfrom
feat/bulk-issues-20260226

Conversation

@ryota-murakami
Copy link
Contributor

@ryota-murakami ryota-murakami commented Feb 25, 2026

Summary

Changes

Issue #151 — NoteModal Decomposition

File Role
src/hooks/board/useNoteModalDraft.ts Draft persistence hook (note/links state, Redux sync, save/close)
src/components/Modals/NoteSection.tsx Pure presentational: PlateEditor + char count + draft timestamp
src/components/Modals/LinkManager.tsx Link CRUD: single-edit coordination, undo delete, user presets
src/components/Modals/NoteModal.tsx Refactored: composes hook + 2 sub-components (363 → 130 lines)

Issue #152 — EditableUrlItem Hook Extraction

File Role
src/hooks/ui/useEditableUrl.ts Encapsulates all editing lifecycle (state, validation, focus, a11y)
src/components/ui/editable-url-item.tsx Refactored: 1 hook call + pure JSX (26 → 1 hook in component)

Test plan

  • TypeScript: no errors
  • ESLint: 0 warnings
  • Unit tests: 1282 passed
  • Build: success
  • E2E: 12/12 shards passed

Closes #151, closes #152

Summary by CodeRabbit

  • New Features

    • Link Manager for adding/editing/deleting links with single-edit enforcement and undo
    • Create dialog for custom link-type presets
    • Note editor section with character count and draft timestamp; draft persistence
  • Quality / UX

    • Improved URL validation, clearer save/cancel feedback, and smoother save/undo flows

…152)

NoteModal (363→130 lines):
- Extract useNoteModalDraft hook for draft persistence
- Extract NoteSection for note editing UI
- Extract LinkManager for link CRUD with undo support

EditableUrlItem (26→1 hook in component):
- Extract useEditableUrl hook encapsulating all editing lifecycle
- Component becomes pure render with single hook call
@vercel
Copy link
Contributor

vercel bot commented Feb 25, 2026

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

Project Deployment Actions Updated (UTC)
gitbox Ready Ready Preview, Comment Feb 25, 2026 7:01pm

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Warning

Rate limit exceeded

@ryota-murakami has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 40 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, 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 the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between d89a328 and be834fd.

📒 Files selected for processing (2)
  • src/components/Modals/LinkManager.tsx
  • src/hooks/ui/useEditableUrl.ts
📝 Walkthrough

Walkthrough

Extracts note editing and link management from NoteModal into NoteSection and LinkManager, adds useNoteModalDraft hook for draft/persistence, and moves EditableUrlItem logic into a new useEditableUrl hook; NoteModal now composes the new pieces.

Changes

Cohort / File(s) Summary
Modals: Note split & composition
src/components/Modals/NoteModal.tsx, src/components/Modals/NoteSection.tsx, src/components/Modals/LinkManager.tsx
NoteModal refactored to compose NoteSection and LinkManager; NoteSection implements editor, char count and draft timestamp; LinkManager implements link CRUD, presets, single-edit coordination, and delete-with-undo.
Editable URL extraction
src/components/ui/editable-url-item.tsx, src/hooks/ui/useEditableUrl.ts
EditableUrlItem internal editing, validation, focus, save/cancel and undo logic moved into new useEditableUrl hook; component now delegates lifecycle to the hook.
Note modal draft hook & exports
src/hooks/board/useNoteModalDraft.ts, src/hooks/board/index.ts
Adds useNoteModalDraft hook (NOTE_MAX_LENGTH, NOTE_WARNING_THRESHOLD) managing note/links state, validation, Redux draft persistence, save/close handlers; re-exported from hooks index.
Surface & wiring updates
src/components/Modals/*, src/components/ui/*
Components re-compose to consume new hooks/components; CreateLinkTypeDialog moved under LinkManager; external prop signatures preserved where applicable.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant NoteSection
  participant LinkManager
  participant useNoteModalDraft
  participant Redux
  participant Server
  participant Toast

  User->>NoteSection: edit note
  NoteSection->>useNoteModalDraft: handleNoteChange(payload)
  User->>LinkManager: add/edit/delete link
  LinkManager->>useNoteModalDraft: handleLinksChange(links)
  useNoteModalDraft->>Redux: persist draft
  User->>NoteSection: click Save
  NoteSection->>useNoteModalDraft: handleSave()
  useNoteModalDraft->>Server: onSave({note, links})
  Server-->>useNoteModalDraft: success / error
  useNoteModalDraft->>Redux: clear draft (on success)
  useNoteModalDraft->>Toast: show success / error
  Toast-->>User: notification
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

A modal split, two parts stand tall,
Notes find order, links answer the call,
Hooks hum quietly, state now neat,
Undo and presets keep the flow sweet,
Small changes, cleaner code — a tidy ball 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the two main refactoring efforts: decomposing NoteModal and extracting useEditableUrl hook, with issue references.
Linked Issues check ✅ Passed All objectives from #151 and #152 are met: NoteModal split into NoteSection and LinkManager, persistence logic extracted to useNoteModalDraft hook, editable-url-item hook count reduced from 26 to single hook usage, and all tests passing.
Out of Scope Changes check ✅ Passed All code changes directly support the two linked issues: new hook exports (useNoteModalDraft, useEditableUrl), component decomposition (NoteSection, LinkManager), and refactored EditableUrlItem—no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/bulk-issues-20260226

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-commenter
Copy link

codecov-commenter commented Feb 25, 2026

Codecov Report

❌ Patch coverage is 63.72881% with 107 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.06%. Comparing base (b2c50d7) to head (be834fd).

Files with missing lines Patch % Lines
src/hooks/ui/useEditableUrl.ts 63.92% 57 Missing ⚠️
src/components/Modals/LinkManager.tsx 39.39% 40 Missing ⚠️
src/hooks/board/useNoteModalDraft.ts 83.33% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #158      +/-   ##
==========================================
- Coverage   69.09%   69.06%   -0.03%     
==========================================
  Files         149      153       +4     
  Lines        4607     4636      +29     
  Branches     1269     1274       +5     
==========================================
+ Hits         3183     3202      +19     
- Misses       1401     1411      +10     
  Partials       23       23              

☔ 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

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

🧹 Nitpick comments (5)
src/hooks/board/useNoteModalDraft.ts (3)

204-206: handleClose wrapping is redundant.

This useCallback just forwards to onClose with no added logic. You could pass onClose directly to consumers and drop the wrapper.

♻️ Simplify
-  const handleClose = useCallback(() => {
-    onClose()
-  }, [onClose])
+  // Use onClose directly in the return object

And in the return:

-    handleClose,
+    handleClose: onClose,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/board/useNoteModalDraft.ts` around lines 204 - 206, The handleClose
useCallback is redundant because it only forwards to onClose; remove the
handleClose wrapper in useNoteModalDraft and pass the original onClose directly
to consumers (e.g., where handleClose was returned/used). Update any references
to handleClose to use onClose (in the hook's return value and any internal
usages) and delete the handleClose declaration to eliminate unnecessary
indirection.

213-215: isFormValid runs isValidUrl on every render for all links.

This is computed inline (not memoized). For a small number of links it's negligible, but if isValidUrl does regex or URL parsing, wrapping in useMemo with [links] as a dependency would be more consistent with the memoized charCount above.

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

In `@src/hooks/board/useNoteModalDraft.ts` around lines 213 - 215, The isFormValid
boolean is recomputed on every render by calling isValidUrl for each link; wrap
this computation in a useMemo to avoid unnecessary work by memoizing the result
with links as the dependency array. Replace the inline const isFormValid =
links.every(...) with a const isFormValid = useMemo(() => links.every(link =>
!link.url || isValidUrl(link.url).valid), [links]) so it only re-evaluates when
links changes, similar to the existing charCount memoization.

141-171: Stale closure risk between handleNoteChange and handleLinksChange.

handleNoteChange captures links and handleLinksChange captures note. If both are called in rapid succession within the same render, the Redux draft dispatch may use a stale snapshot of the other value. For a draft-persistence mechanism this is low-risk (the next keystroke corrects it), but worth noting.

A common fix is to use a ref mirror for the "other" value, or consolidate into a single update function. Given the low severity, this is fine to defer.

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

In `@src/hooks/board/useNoteModalDraft.ts` around lines 141 - 171,
handleNoteChange captures links and handleLinksChange captures note, risking
stale values when both fire quickly; fix by introducing refs (e.g., noteRef and
linksRef) that are kept in sync inside setNote and setLinks setters, then read
from those refs when calling dispatch(updateDraftNote(...)) in both
handleNoteChange and handleLinksChange (or alternatively consolidate into a
single updateDraft function that accepts both content and links and is always
called with current ref values) so dispatch always uses the latest note and
links.
src/hooks/ui/useEditableUrl.ts (1)

190-247: handleSave treats onUrlChange as synchronous — verify this assumption holds.

The save flow calls onUrlChange(trimmedValue) (line 216) then immediately sets state (lines 219-222). If a consumer ever makes onUrlChange asynchronous, this will break. The current callers in LinkManager are synchronous state updates, so this is fine today.

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

In `@src/hooks/ui/useEditableUrl.ts` around lines 190 - 247, handleSave currently
assumes onUrlChange is synchronous; make it robust to async handlers by treating
onUrlChange(trimmedValue) as possibly returning a Promise: convert handleSave to
an async function, await the result of onUrlChange and wrap it in try/catch to
handle rejections (setting error/announcement and clearing isSaving/isSavingRef
appropriately), keep isMountedRef checks before updating state, and preserve the
success timeout logic; update the dependency array to include any new
identifiers you reference (e.g., isMountedRef) and ensure
debouncedValidate.cancel and restoreFocus behavior remains unchanged.
src/components/Modals/LinkManager.tsx (1)

56-70: Preset loading uses useState + useEffectuse() with Suspense is preferred per project conventions.

Per the project's CLAUDE.md rules, React 19.2 patterns (use(), Suspense) are preferred over useState + useEffect for data fetching. That said, this is a non-blocking side load that silently falls back, so restructuring may not be worth it for this refactoring PR.

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

In `@src/components/Modals/LinkManager.tsx` around lines 56 - 70, Replace the
useState/useEffect pattern that fetches presets with React 19.2's use() +
Suspense: remove the mounted flag and the getUserPresets().then(...) effect;
import use from React and call const userPresets = try { use(getUserPresets()) }
catch (e) { undefined } (or wrap the use() call in a try/catch) and render using
userPresets ?? builtInPresets so the component falls back silently on error;
update references from setUserPresets to use the userPresets local variable and
ensure the component is rendered inside a Suspense boundary.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Modals/LinkManager.tsx`:
- Around line 164-184: The list key uses the array index
(key={`${link.type}-${index}`}) which can make React reuse wrong EditableUrlItem
instances after operations like handleUndoDelete; change to a stable unique key
by adding an id to each ProjectLink and use that id for the key (or generate a
stable uuid when creating a new link in the code paths that add/splice links
such as handleRemoveUrl, handleUndoDelete, and wherever new links are appended),
and update any construction of links so new items always include that id; keep
EditableUrlItem props (autoEdit, forceExitEdit, onUrlChange/onTypeChange) intact
so component instances remain properly matched.
- Around line 122-134: handleRemoveUrl doesn't adjust editingUrlIndex when an
earlier link is removed, causing the wrong item to be treated as edited; update
handleRemoveUrl to compute and set a corrected editing index: if editingUrlIndex
is not null and the removed index is less than editingUrlIndex, call
setEditingUrlIndex(editingUrlIndex - 1); if the removed index equals
editingUrlIndex keep the existing behavior (set null); then proceed to remove
the link and call onLinksChange with newLinks so forceExitEdit will target the
correct item.

In `@src/hooks/ui/useEditableUrl.ts`:
- Around line 84-92: The UseEditableUrlParams interface currently includes a
disabled property that is never used by the hook; remove disabled from the
UseEditableUrlParams definition and any related types so the hook signature
(useEditableUrl) no longer accepts or suggests the disabled param, and update
any call sites or tests that pass disabled into useEditableUrl to instead handle
disabled at the component/JSX level.

---

Nitpick comments:
In `@src/components/Modals/LinkManager.tsx`:
- Around line 56-70: Replace the useState/useEffect pattern that fetches presets
with React 19.2's use() + Suspense: remove the mounted flag and the
getUserPresets().then(...) effect; import use from React and call const
userPresets = try { use(getUserPresets()) } catch (e) { undefined } (or wrap the
use() call in a try/catch) and render using userPresets ?? builtInPresets so the
component falls back silently on error; update references from setUserPresets to
use the userPresets local variable and ensure the component is rendered inside a
Suspense boundary.

In `@src/hooks/board/useNoteModalDraft.ts`:
- Around line 204-206: The handleClose useCallback is redundant because it only
forwards to onClose; remove the handleClose wrapper in useNoteModalDraft and
pass the original onClose directly to consumers (e.g., where handleClose was
returned/used). Update any references to handleClose to use onClose (in the
hook's return value and any internal usages) and delete the handleClose
declaration to eliminate unnecessary indirection.
- Around line 213-215: The isFormValid boolean is recomputed on every render by
calling isValidUrl for each link; wrap this computation in a useMemo to avoid
unnecessary work by memoizing the result with links as the dependency array.
Replace the inline const isFormValid = links.every(...) with a const isFormValid
= useMemo(() => links.every(link => !link.url || isValidUrl(link.url).valid),
[links]) so it only re-evaluates when links changes, similar to the existing
charCount memoization.
- Around line 141-171: handleNoteChange captures links and handleLinksChange
captures note, risking stale values when both fire quickly; fix by introducing
refs (e.g., noteRef and linksRef) that are kept in sync inside setNote and
setLinks setters, then read from those refs when calling
dispatch(updateDraftNote(...)) in both handleNoteChange and handleLinksChange
(or alternatively consolidate into a single updateDraft function that accepts
both content and links and is always called with current ref values) so dispatch
always uses the latest note and links.

In `@src/hooks/ui/useEditableUrl.ts`:
- Around line 190-247: handleSave currently assumes onUrlChange is synchronous;
make it robust to async handlers by treating onUrlChange(trimmedValue) as
possibly returning a Promise: convert handleSave to an async function, await the
result of onUrlChange and wrap it in try/catch to handle rejections (setting
error/announcement and clearing isSaving/isSavingRef appropriately), keep
isMountedRef checks before updating state, and preserve the success timeout
logic; update the dependency array to include any new identifiers you reference
(e.g., isMountedRef) and ensure debouncedValidate.cancel and restoreFocus
behavior remains unchanged.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2c50d7 and be0682a.

📒 Files selected for processing (7)
  • src/components/Modals/LinkManager.tsx
  • src/components/Modals/NoteModal.tsx
  • src/components/Modals/NoteSection.tsx
  • src/components/ui/editable-url-item.tsx
  • src/hooks/board/index.ts
  • src/hooks/board/useNoteModalDraft.ts
  • src/hooks/ui/useEditableUrl.ts

@github-actions
Copy link

github-actions bot commented Feb 25, 2026

🧪 E2E Coverage Report (Sharded: 12 parallel jobs)

Metric Coverage
Lines 93.75%
Functions 17.98%
Branches 17.57%
Statements 31%

📊 Full report available in workflow artifacts

@ryota-murakami
Copy link
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

✅ Actions performed

Full review triggered.

Copy link

@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

♻️ Duplicate comments (3)
src/components/Modals/LinkManager.tsx (2)

164-184: Index-based list key after undo-insert can cause React to reuse wrong component instances.

key={\${link.type}-${index}`}will mismatch afterhandleUndoDeletesplices a link back at a prior position. Consider adding a stableidtoProjectLink` if this causes visible UI glitches.

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

In `@src/components/Modals/LinkManager.tsx` around lines 164 - 184, The list key
for EditableUrlItem uses an unstable index-based key
(`key={`${link.type}-${index}`}`) which can cause React to reuse wrong component
instances after handleUndoDelete modifies the array; update the ProjectLink
items to include and persist a stable unique identifier (e.g., id) and change
the key to use that stable id (for example reference the link.id in the key for
the EditableUrlItem inside LinkManager.tsx) so that adding/splicing via
handleRemoveUrl and handleUndoDelete preserves component identity.

122-134: ⚠️ Potential issue | 🟠 Major

editingUrlIndex becomes stale when deleting a link before the currently-edited item.

Deleting index i where i < editingUrlIndex shifts all subsequent items down by one, but editingUrlIndex is not adjusted. This causes forceExitEdit (line 179-181) to target the wrong item.

🐛 Proposed fix
   const handleRemoveUrl = useCallback(
     (index: number) => {
       const linkToDelete = links[index]
       if (!linkToDelete) return
       setDeletedLink({ link: linkToDelete, index })
       const newLinks = links.filter((_, i) => i !== index)
       if (editingUrlIndex === index) {
         setEditingUrlIndex(null)
+      } else if (editingUrlIndex !== null && index < editingUrlIndex) {
+        setEditingUrlIndex(editingUrlIndex - 1)
       }
       onLinksChange(newLinks)
     },
     [editingUrlIndex, links, onLinksChange],
   )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Modals/LinkManager.tsx` around lines 122 - 134, When removing
a link in handleRemoveUrl, adjust editingUrlIndex when the deleted index is
before the currently edited item: if linkToDelete is found and editingUrlIndex
!== null, setEditingUrlIndex(null) when index === editingUrlIndex, otherwise if
index < editingUrlIndex call setEditingUrlIndex(editingUrlIndex - 1) so the
edited item stays in sync after filtering; perform this update (alongside
setDeletedLink and before onLinksChange) to avoid forceExitEdit referencing the
wrong item. Reference: handleRemoveUrl, editingUrlIndex, setEditingUrlIndex,
setDeletedLink, onLinksChange, links.
src/hooks/ui/useEditableUrl.ts (1)

84-92: Unused disabled parameter still present.

disabled is declared in UseEditableUrlParams (line 20) but never destructured or used in the hook. The component handles it directly in JSX.

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

In `@src/hooks/ui/useEditableUrl.ts` around lines 84 - 92, Remove the unused
"disabled" parameter from the type and hook API: delete "disabled" from the
UseEditableUrlParams interface definition and from any related type exports, and
ensure useEditableUrl's signature no longer expects or references "disabled" (so
the hook's parameter list remains { link, onUrlChange, onDelete, onUndoDelete,
autoEdit = false, onEditStart, forceExitEdit = false }). Also update any call
sites or tests that pass "disabled" into useEditableUrl to stop passing it,
since the component handles disabled in JSX.
🧹 Nitpick comments (3)
src/hooks/ui/useEditableUrl.ts (1)

359-362: handleSavePointerDown sets isCancellingRef — naming is misleading.

This handler fires on the save button's pointerDown, but it sets isCancellingRef = true, which tells handleBlur to skip its auto-save. The mechanism is correct (prevents double-save from blur + click), but the ref name suggests cancellation when it's really "suppress blur-triggered save."

A name like isSuppressingBlurSaveRef or skipBlurSaveRef would better convey intent.

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

In `@src/hooks/ui/useEditableUrl.ts` around lines 359 - 362, The handler
handleSavePointerDown currently sets isCancellingRef.current = true which is
misleading; rename the ref to something like skipBlurSaveRef or
isSuppressingBlurSaveRef and update all references (including where handleBlur
checks it) so the intent — suppressing the blur-triggered auto-save when the
save button's pointerDown fires — is clear; change the ref declaration name, its
.current assignments in handleSavePointerDown, and the conditional check in
handleBlur (and any other usages) to the new identifier and update any related
comments to match.
src/components/Modals/LinkManager.tsx (1)

56-70: User presets fetch could use the React 19 use() + Suspense pattern.

Currently loads presets via useEffect + setState. The project guidelines prefer React 19.2 patterns (use API, Suspense). This is a good candidate for a future refactor but not blocking. Based on learnings, the project prefers React 19.2 APIs like use over useState + useEffect for data fetching.

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

In `@src/components/Modals/LinkManager.tsx` around lines 56 - 70, The useEffect +
setState pattern in LinkManager (useEffect, getUserPresets, setUserPresets)
should be refactored to React 19's data fetching pattern using the use() hook
and Suspense: replace the effect-based fetch with a resource created outside the
component (or a server/loader function) that returns the preset promise, call
use(getUserPresetsPromise) inside LinkManager to read presets synchronously, and
wrap LinkManager (or the presets-consuming subtree) with a Suspense boundary to
show a fallback while loading; ensure error handling is moved into an error
boundary or the promise rejection is handled by the provider so the old
silent-fail behavior is preserved.
src/hooks/board/useNoteModalDraft.ts (1)

141-171: Stale closure risk between handleNoteChange and handleLinksChange.

handleNoteChange captures links (line 156) and handleLinksChange captures note (line 170). Each dispatches updateDraftNote with the other's value from the closure. If rapid edits overlap, one dispatch could overwrite the other's latest value in Redux.

This is unlikely in practice (users edit note or links, not both simultaneously), but a useRef for the latest values would eliminate the risk entirely.

♻️ Proposed ref-based fix
+ const noteRef = useRef(note)
+ noteRef.current = note
+ const linksRef = useRef(links)
+ linksRef.current = links
+
  const handleNoteChange = useCallback(
    (value: string) => {
      try {
        const slateValue = parseSlateValue(value)
        const textLength = getSlateTextLength(slateValue)

        if (textLength <= NOTE_MAX_LENGTH) {
          setNote(value)
-         dispatch(updateDraftNote({ cardId, content: value, links }))
+         dispatch(updateDraftNote({ cardId, content: value, links: linksRef.current }))
        }
      } catch {
        setNote(value)
-       dispatch(updateDraftNote({ cardId, content: value, links }))
+       dispatch(updateDraftNote({ cardId, content: value, links: linksRef.current }))
      }
    },
-   [cardId, dispatch, links],
+   [cardId, dispatch],
  )

  const handleLinksChange = useCallback(
    (newLinks: ProjectLink[]) => {
      setLinks(newLinks)
-     dispatch(updateDraftNote({ cardId, content: note, links: newLinks }))
+     dispatch(updateDraftNote({ cardId, content: noteRef.current, links: newLinks }))
    },
-   [cardId, dispatch, note],
+   [cardId, dispatch],
  )

This also stabilizes callback identity, preventing LinkManager re-renders on every note keystroke.

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

In `@src/hooks/board/useNoteModalDraft.ts` around lines 141 - 171,
handleNoteChange and handleLinksChange capture stale closures for links and note
respectively causing race overwrites when dispatching updateDraftNote; fix by
adding refs (e.g., latestNoteRef and latestLinksRef) that are updated whenever
setNote or setLinks are called, then have both handlers read from those refs
when dispatching updateDraftNote, and remove note/links from the handlers'
dependency arrays so handleNoteChange and handleLinksChange remain stable (keep
cardId and dispatch in deps); ensure you update the refs in the places that call
setNote/setLinks so the refs always reflect latest values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Modals/NoteSection.tsx`:
- Line 46: The Label's htmlFor="note" doesn't match any input; update the
NoteSection so the label either is not using htmlFor or is linked via
aria-labelledby: remove htmlFor="note" from the Label in NoteSection (or change
it to id="note-label"), then add aria-labelledby="note-label" to the PlateEditor
root/container (or to the element that PlateEditor renders) so the label and
editor are properly associated; reference the Label component and the
PlateEditor usage in NoteSection when making the change.

---

Duplicate comments:
In `@src/components/Modals/LinkManager.tsx`:
- Around line 164-184: The list key for EditableUrlItem uses an unstable
index-based key (`key={`${link.type}-${index}`}`) which can cause React to reuse
wrong component instances after handleUndoDelete modifies the array; update the
ProjectLink items to include and persist a stable unique identifier (e.g., id)
and change the key to use that stable id (for example reference the link.id in
the key for the EditableUrlItem inside LinkManager.tsx) so that adding/splicing
via handleRemoveUrl and handleUndoDelete preserves component identity.
- Around line 122-134: When removing a link in handleRemoveUrl, adjust
editingUrlIndex when the deleted index is before the currently edited item: if
linkToDelete is found and editingUrlIndex !== null, setEditingUrlIndex(null)
when index === editingUrlIndex, otherwise if index < editingUrlIndex call
setEditingUrlIndex(editingUrlIndex - 1) so the edited item stays in sync after
filtering; perform this update (alongside setDeletedLink and before
onLinksChange) to avoid forceExitEdit referencing the wrong item. Reference:
handleRemoveUrl, editingUrlIndex, setEditingUrlIndex, setDeletedLink,
onLinksChange, links.

In `@src/hooks/ui/useEditableUrl.ts`:
- Around line 84-92: Remove the unused "disabled" parameter from the type and
hook API: delete "disabled" from the UseEditableUrlParams interface definition
and from any related type exports, and ensure useEditableUrl's signature no
longer expects or references "disabled" (so the hook's parameter list remains {
link, onUrlChange, onDelete, onUndoDelete, autoEdit = false, onEditStart,
forceExitEdit = false }). Also update any call sites or tests that pass
"disabled" into useEditableUrl to stop passing it, since the component handles
disabled in JSX.

---

Nitpick comments:
In `@src/components/Modals/LinkManager.tsx`:
- Around line 56-70: The useEffect + setState pattern in LinkManager (useEffect,
getUserPresets, setUserPresets) should be refactored to React 19's data fetching
pattern using the use() hook and Suspense: replace the effect-based fetch with a
resource created outside the component (or a server/loader function) that
returns the preset promise, call use(getUserPresetsPromise) inside LinkManager
to read presets synchronously, and wrap LinkManager (or the presets-consuming
subtree) with a Suspense boundary to show a fallback while loading; ensure error
handling is moved into an error boundary or the promise rejection is handled by
the provider so the old silent-fail behavior is preserved.

In `@src/hooks/board/useNoteModalDraft.ts`:
- Around line 141-171: handleNoteChange and handleLinksChange capture stale
closures for links and note respectively causing race overwrites when
dispatching updateDraftNote; fix by adding refs (e.g., latestNoteRef and
latestLinksRef) that are updated whenever setNote or setLinks are called, then
have both handlers read from those refs when dispatching updateDraftNote, and
remove note/links from the handlers' dependency arrays so handleNoteChange and
handleLinksChange remain stable (keep cardId and dispatch in deps); ensure you
update the refs in the places that call setNote/setLinks so the refs always
reflect latest values.

In `@src/hooks/ui/useEditableUrl.ts`:
- Around line 359-362: The handler handleSavePointerDown currently sets
isCancellingRef.current = true which is misleading; rename the ref to something
like skipBlurSaveRef or isSuppressingBlurSaveRef and update all references
(including where handleBlur checks it) so the intent — suppressing the
blur-triggered auto-save when the save button's pointerDown fires — is clear;
change the ref declaration name, its .current assignments in
handleSavePointerDown, and the conditional check in handleBlur (and any other
usages) to the new identifier and update any related comments to match.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2c50d7 and be0682a.

📒 Files selected for processing (7)
  • src/components/Modals/LinkManager.tsx
  • src/components/Modals/NoteModal.tsx
  • src/components/Modals/NoteSection.tsx
  • src/components/ui/editable-url-item.tsx
  • src/hooks/board/index.ts
  • src/hooks/board/useNoteModalDraft.ts
  • src/hooks/ui/useEditableUrl.ts

- Fix stale editingUrlIndex after link deletion in LinkManager
- Remove unused disabled param from useEditableUrl interface
- Replace invalid htmlFor="note" with id="note-label" in NoteSection
Copy link

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

🧹 Nitpick comments (1)
src/hooks/ui/useEditableUrl.ts (1)

7-7: Consider decoupling hook types from component modules.

useEditableUrl imports ProjectLink from a UI component file, which inverts dependency direction (hooks → components). Moving ProjectLink to a shared types module would keep boundaries cleaner.

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

In `@src/hooks/ui/useEditableUrl.ts` at line 7, The hook useEditableUrl currently
imports the ProjectLink type from the UI component editable-url-item which
inverts dependencies; extract ProjectLink into a shared types module (e.g.,
types/project.ts or types/ui.ts), export the type there, and update
useEditableUrl to import ProjectLink from that new shared module; also update
the component file editable-url-item to import ProjectLink from the shared types
module and run a quick search to update any other imports referencing the old
location.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Modals/LinkManager.tsx`:
- Around line 75-77: handleAddUrl appends a blank link but never updates
editingUrlIndex, so the newly added item immediately gets forceExitEdit; after
adding the new link (in handleAddUrl and the other add-link handler around the
second occurrence), call setEditingUrlIndex(links.length) (or the equivalent
state setter) so the new item's index becomes the active editingUrlIndex, and
ensure you use the current links array when computing the new index before
calling onLinksChange.
- Around line 141-148: The undo handler (handleUndoDelete) reinserts a deleted
link but doesn't update editingUrlIndex, so if the restored item is inserted
before the currently edited item the edit index becomes stale; modify
handleUndoDelete to after computing index (const { link, index } = deletedLink)
and before calling setDeletedLink(null) / onLinksChange(newLinks) check if
editingUrlIndex is not null/undefined and index <= editingUrlIndex then call
setEditingUrlIndex(prev => prev + 1) (or set to index-adjusted value) to shift
the edit index right by one; ensure you reference the state setters
editingUrlIndex and setEditingUrlIndex in the useCallback deps list so the
closure sees the latest value.

In `@src/hooks/ui/useEditableUrl.ts`:
- Around line 271-276: The save path in useEditableUrl can bypass in-flight
debounced validation because it only checks the mutable error state; modify the
block where trimmedEdit is compared to trimmedLink (and
debouncedValidate.cancel() is called) to run the underlying validation
synchronously for trimmedEdit before setting isSavingRef.current or calling
onUrlChange: invoke the same validator used by debouncedValidate (e.g.,
validateUrl or the validation helper inside this hook) and if it returns an
error, set the error state and abort (do not call onUrlChange); only proceed to
set isSavingRef.current = true, cancel debouncedValidate, and call
onUrlChange(trimmedEdit) when the synchronous validation passes and
isMountedRef.current remains true.

---

Nitpick comments:
In `@src/hooks/ui/useEditableUrl.ts`:
- Line 7: The hook useEditableUrl currently imports the ProjectLink type from
the UI component editable-url-item which inverts dependencies; extract
ProjectLink into a shared types module (e.g., types/project.ts or types/ui.ts),
export the type there, and update useEditableUrl to import ProjectLink from that
new shared module; also update the component file editable-url-item to import
ProjectLink from the shared types module and run a quick search to update any
other imports referencing the old location.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be0682a and d89a328.

📒 Files selected for processing (4)
  • src/components/Modals/LinkManager.tsx
  • src/components/Modals/NoteSection.tsx
  • src/components/ui/editable-url-item.tsx
  • src/hooks/ui/useEditableUrl.ts

- Set editingUrlIndex when adding new URL to prevent forceExitEdit conflict
- Adjust editingUrlIndex on undo reinsert to prevent stale index
- Add synchronous validation in force-exit path to prevent invalid URL save
@ryota-murakami ryota-murakami merged commit d0ed43a into main Feb 25, 2026
20 checks passed
@ryota-murakami ryota-murakami deleted the feat/bulk-issues-20260226 branch February 25, 2026 19:16
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.

Refactor editable-url-item: reduce 32 hooks to manageable count Extract NoteModal into NoteSection + LinkManager components

2 participants