fix(chat): React #310 — ChatSection вынесен на module level вместо useCallback

useCallback для ChatSection стоял после раннего return (if (!task)) —
в рендерах без задачи хук не вызывался → «Rendered more hooks than during
the previous render». Правильное решение: компонент на module level с пропом
taskId (стабильный тип + нет зависимости от хуков).
This commit is contained in:
2026-09-21 22:10:57 +03:00
parent 693834ac27
commit 13db5be030

View File

@@ -31,6 +31,21 @@ import { DeleteTaskDialog } from '@/components/task-detail/DeleteTaskDialog';
import { useTaskFieldConditions } from '@/components/task-detail/useTaskFieldConditions';
import type { FormData, FormTab, FormStatus, FormTransition, TaskAssigneeEntry, TabModuleInfo, TaskDetailResponse, OrgRole } from '@/components/task-detail/types';
// Компонент чата (без заголовка — он вынесен в полосу вкладок).
// ВАЖНО: объявлен на module level, а не inline в теле TaskDetail — компонент,
// объявленный внутри другого компонента, получает новый тип на каждый рендер,
// и React полностью перемонтирует чат (сброс скролла и состояния) при ЛЮБОМ
// ре-рендере TaskDetail. Именно этот remount и был первопричиной «дёрганья» чата.
function ChatSection({ className = '', taskId }: { className?: string; taskId: number }) {
return (
<div className={`flex flex-col bg-background ${className}`}>
<div className="flex-1 overflow-hidden">
<TaskChat taskId={taskId} />
</div>
</div>
);
}
const TaskDetail = () => {
const { id, taskId } = useParams<{ id: string; taskId: string }>();
const [, setLocation] = useLocation();
@@ -575,20 +590,11 @@ const TaskDetail = () => {
formNamesMap[f.id] = f.name;
});
// Компонент чата (без заголовка — он вынесен в полосу вкладок).
// ВАЖНО: обязательно useCallback со стабильной идентичностью — компонент,
// объявленный inline в теле рендера, получает НОВЫЙ тип на каждый рендер,
// и React полностью перемонтирует чат (сброс скролла и состояния) при ЛЮБОМ
// ре-рендере TaskDetail (SSE-событие, refetch задачи, возврат на вкладку).
// Именно этот remount и был первопричиной «дёрганья» чата. Тот же паттерн
// уже исправлен для мобильного accordion (см. комментарий ниже).
const ChatSection = useCallback(({ className = '' }: { className?: string }) => (
<div className={`flex flex-col bg-background ${className}`}>
<div className="flex-1 overflow-hidden">
<TaskChat taskId={taskIdNumber} />
</div>
</div>
), [taskIdNumber]);
// Компонент чата вынесен на module level (см. объявление ChatSection выше) —
// объявленный inline в теле рендера компонент получает НОВЫЙ тип на каждый
// рендер, и React полностью перемонтирует чат (сброс скролла и состояния)
// при ЛЮБОМ ре-рендере TaskDetail. Именно этот remount и был первопричиной
// «дёрганья» чата. Тот же паттерн уже исправлен для мобильного accordion.
const taskContent = (
<TaskContent
@@ -766,7 +772,7 @@ const TaskDetail = () => {
{chatLayout === 'left' && (
<>
{chatEnabled && expandedPane !== 'task' && (
<ChatSection className={expandedPane === 'chat' ? "flex-1" : "w-1/3 border-r"} />
<ChatSection className={expandedPane === 'chat' ? "flex-1" : "w-1/3 border-r"} taskId={taskIdNumber} />
)}
{expandedPane !== 'chat' && (
<div className={(!chatEnabled || expandedPane === 'task') ? "flex-1 px-3 md:px-4 py-4 overflow-auto" : "w-2/3 px-3 md:px-4 py-4 overflow-auto"}>
@@ -780,7 +786,7 @@ const TaskDetail = () => {
{chatLayout === 'tab' && (
<>
{selectedTabId === 'chat' && chatEnabled ? (
<ChatSection className="flex-1" />
<ChatSection className="flex-1" taskId={taskIdNumber} />
) : (
<div className="flex-1 px-3 md:px-4 py-4 overflow-auto">
{isMobile ? mobileTaskAccordion : taskContent}