fix: resolve ESLint errors for React compiler rules

- Chat.tsx: replace mutable variable in render IIFE with useMemo+reduce
  to satisfy react-hooks/immutability rule
- ConnectionBanner.tsx: move setState calls into a useCallback to avoid
  synchronous setState in effect body (react-hooks/set-state-in-effect)
- useGateway.ts: suppress set-state-in-effect for legitimate mount init
This commit is contained in:
Nicolas Varrot
2026-02-12 01:42:40 +00:00
parent 375bd102d4
commit 29482e377a
4 changed files with 44 additions and 19 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useCallback, useState } from 'react';
import { useEffect, useRef, useCallback, useState, useMemo } from 'react';
import { ChatMessageComponent } from './ChatMessage';
import { ChatInput } from './ChatInput';
import { TypingIndicator } from './TypingIndicator';
@@ -112,6 +112,16 @@ export function Chat({ messages, isGenerating, status, onSend, onAbort }: Props)
onSend(text, attachments);
}, [onSend]);
const visibleMessages = useMemo(() => {
const filtered = messages.filter(hasVisibleContent);
return filtered.reduce<Array<{ msg: ChatMessage; showSep: boolean }>>((acc, msg) => {
const dk = getDateKey(msg.timestamp);
const prevDk = acc.length > 0 ? getDateKey(acc[acc.length - 1].msg.timestamp) : '';
acc.push({ msg, showSep: dk !== prevDk });
return acc;
}, []);
}, [messages]);
const showTyping = isGenerating && !hasStreamedText(messages);
return (
@@ -130,13 +140,7 @@ export function Chat({ messages, isGenerating, status, onSend, onAbort }: Props)
<div className="text-sm mt-1 text-zinc-500">{t('chat.welcomeSub')}</div>
</div>
)}
{(() => {
let lastDateKey = '';
return messages.filter(hasVisibleContent).map(msg => {
const dk = getDateKey(msg.timestamp);
const showSep = dk !== lastDateKey;
lastDateKey = dk;
return (
{visibleMessages.map(({ msg, showSep }) => (
<div key={msg.id}>
{showSep && (
<div className="flex items-center gap-3 py-3 px-4 select-none" aria-label={formatDateSeparator(msg.timestamp, t)}>
@@ -147,9 +151,7 @@ export function Chat({ messages, isGenerating, status, onSend, onAbort }: Props)
)}
<ChatMessageComponent message={msg} onRetry={!isGenerating ? handleSend : undefined} />
</div>
);
});
})()}
))}
{showTyping && <TypingIndicator />}
<div ref={bottomRef} />
</div>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import { Wifi, Loader2 } from 'lucide-react';
import type { ConnectionStatus } from '../types';
import { useT } from '../hooks/useLocale';
@@ -15,28 +15,31 @@ export function ConnectionBanner({ status }: Props) {
const prevStatus = useRef<ConnectionStatus | null>(null);
const dismissTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
const prev = prevStatus.current;
prevStatus.current = status;
const updateBanner = useCallback((prev: ConnectionStatus | null, current: ConnectionStatus) => {
if (dismissTimer.current) {
clearTimeout(dismissTimer.current);
dismissTimer.current = null;
}
if (status === 'disconnected' || status === 'connecting') {
if (current === 'disconnected' || current === 'connecting') {
if (prev === 'connected') {
setBanner('reconnecting');
}
} else if (status === 'connected' && prev !== null && prev !== 'connected') {
} else if (current === 'connected' && prev !== null && prev !== 'connected') {
setBanner('reconnected');
dismissTimer.current = setTimeout(() => setBanner('hidden'), 3000);
}
}, []);
useEffect(() => {
const prev = prevStatus.current;
prevStatus.current = status;
updateBanner(prev, status);
return () => {
if (dismissTimer.current) clearTimeout(dismissTimer.current);
};
}, [status]);
}, [status, updateBanner]);
if (banner === 'hidden') return null;

View File

@@ -307,6 +307,8 @@ export function useGateway() {
initRef.current = true;
const stored = getStoredCredentials();
if (stored) {
// Init on mount — setupClient sets state as part of establishing the connection
// eslint-disable-next-line react-hooks/set-state-in-effect
setupClient(stored.url, stored.token);
} else {
setAuthenticated(false);