fix(frontend): fix useMemo deps syntax and replace stale ref reads in render body

useMemo received deps as trailing comma-separated args instead of array.
Replace all render-body reads of mentionStateRef.current with reactive
mentionState from useMemo. Keep ref for event listener closures only.
This commit is contained in:
ZhenYi 2026-04-18 11:14:10 +08:00
parent 434a3595eb
commit 989a4117db

View File

@ -223,19 +223,13 @@ export function MentionPopover({
onCategoryEnterRef.current = onCategoryEnter;
// Parse mention state
mentionStateRef.current = useMemo(
() => {
const before = inputValue.slice(0, cursorPosition);
const match = before.match(/@([^:@\s<]*)(:([^\s<]*))?$/);
if (!match) return null;
return { category: match[1].toLowerCase(), item: (match[3] ?? '').toLowerCase(), hasColon: match[2] !== undefined };
},
// Inline — not used as dependency
inputValue, cursorPosition,
);
// Force update reference
const [, setTick] = useState(0);
useEffect(() => { setTick(t => t + 1); }, [inputValue, cursorPosition]);
const mentionState = useMemo(() => {
const before = inputValue.slice(0, cursorPosition);
const match = before.match(/@([^:@\s<]*)(:([^\s<]*))?$/);
if (!match) return null;
return { category: match[1].toLowerCase(), item: (match[3] ?? '').toLowerCase(), hasColon: match[2] !== undefined };
}, [inputValue, cursorPosition]);
mentionStateRef.current = mentionState;
visibleSuggestionsRef.current = suggestions;
selectedIndexRef.current = selectedIndex;
@ -314,7 +308,7 @@ export function MentionPopover({
useLayoutEffect(() => {
if (!containerRef.current) return;
if (!mentionStateRef.current) return;
if (!mentionState) return;
const textarea = containerRef.current;
const styles = window.getComputedStyle(textarea);
@ -328,8 +322,7 @@ export function MentionPopover({
tempDiv.style.pointerEvents = 'none';
document.body.appendChild(tempDiv);
const ms = mentionStateRef.current;
const pattern = ms.hasColon ? `@${ms.category}:${ms.item}` : `@${ms.category}`;
const pattern = mentionState.hasColon ? `@${mentionState.category}:${mentionState.item}` : `@${mentionState.category}`;
tempDiv.textContent = inputValue.slice(0, cursorPosition - pattern.length + (inputValue.slice(0, cursorPosition).lastIndexOf(pattern)));
const span = document.createElement('span');
@ -381,7 +374,7 @@ export function MentionPopover({
// Simple approach: just use the textarea bounds + estimated offset
// This is sufficient for most cases
}, [inputValue, cursorPosition, containerRef, mentionStateRef, suggestions.length]);
}, [inputValue, cursorPosition, containerRef, mentionState, suggestions.length]);
// ─── Keyboard Navigation ───────────────────────────────────────────────────
@ -434,18 +427,16 @@ export function MentionPopover({
// Hide when mention state is gone
useEffect(() => {
const ms = mentionStateRef.current;
if (!ms) closePopover();
}, [inputValue, cursorPosition, closePopover]);
if (!mentionState) closePopover();
}, [inputValue, cursorPosition, closePopover, mentionState]);
// Don't render if no valid mention context
const ms = mentionStateRef.current;
if (!ms) return null;
if (!mentionState) return null;
const isLoading = (ms.category === 'repository' && reposLoading) ||
(ms.category === 'ai' && aiConfigsLoading);
const isLoading = (mentionState.category === 'repository' && reposLoading) ||
(mentionState.category === 'ai' && aiConfigsLoading);
const currentCategory = ms.hasColon ? ms.category : null;
const currentCategory = mentionState.hasColon ? mentionState.category : null;
const catConfig = currentCategory ? CATEGORY_CONFIG[currentCategory] : null;
return (
@ -456,12 +447,12 @@ export function MentionPopover({
<span className="text-xs font-semibold text-muted-foreground">@</span>
</div>
<div className="flex-1 flex items-center gap-1.5 min-w-0">
{ms.hasColon ? (
{mentionState.hasColon ? (
<>
<span className={cn('text-xs font-medium px-1.5 py-0.5 rounded-md', catConfig?.bgColor, catConfig?.color)}>{ms.category}</span>
{ms.item && (<>
<span className={cn('text-xs font-medium px-1.5 py-0.5 rounded-md', catConfig?.bgColor, catConfig?.color)}>{mentionState.category}</span>
{mentionState.item && (<>
<span className="text-muted-foreground">/</span>
<span className="text-xs text-foreground font-medium truncate">{ms.item}</span>
<span className="text-xs text-foreground font-medium truncate">{mentionState.item}</span>
</>)}
</>
) : (
@ -488,7 +479,7 @@ export function MentionPopover({
<SuggestionItem key={s.mentionId || s.label + i} suggestion={s} isSelected={i === selectedIndex}
onSelect={() => doInsert(s)}
onMouseEnter={() => { setSelectedIndex(i); selectedIndexRef.current = i; }}
searchTerm={ms.item} />
searchTerm={mentionState.item} />
</div>
))}
</div>