From 13db5be0300ec3be2f0c0dcef8d308cba7a4d6bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=D1=81=20=D0=A1=D1=83=D0=BB=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Mon, 21 Sep 2026 22:10:57 +0300 Subject: [PATCH] =?UTF-8?q?fix(chat):=20React=20#310=20=E2=80=94=20ChatSec?= =?UTF-8?q?tion=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=20=D0=BD?= =?UTF-8?q?=D0=B0=20module=20level=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE?= =?UTF-8?q?=20useCallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useCallback для ChatSection стоял после раннего return (if (!task)) — в рендерах без задачи хук не вызывался → «Rendered more hooks than during the previous render». Правильное решение: компонент на module level с пропом taskId (стабильный тип + нет зависимости от хуков). --- client/src/pages/TaskDetail.tsx | 38 +++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/client/src/pages/TaskDetail.tsx b/client/src/pages/TaskDetail.tsx index b5a1849..c976ad5 100644 --- a/client/src/pages/TaskDetail.tsx +++ b/client/src/pages/TaskDetail.tsx @@ -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 ( +
+
+ +
+
+ ); +} + 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 }) => ( -
-
- -
-
- ), [taskIdNumber]); + // Компонент чата вынесен на module level (см. объявление ChatSection выше) — + // объявленный inline в теле рендера компонент получает НОВЫЙ тип на каждый + // рендер, и React полностью перемонтирует чат (сброс скролла и состояния) + // при ЛЮБОМ ре-рендере TaskDetail. Именно этот remount и был первопричиной + // «дёрганья» чата. Тот же паттерн уже исправлен для мобильного accordion. const taskContent = ( { {chatLayout === 'left' && ( <> {chatEnabled && expandedPane !== 'task' && ( - + )} {expandedPane !== 'chat' && (
@@ -780,7 +786,7 @@ const TaskDetail = () => { {chatLayout === 'tab' && ( <> {selectedTabId === 'chat' && chatEnabled ? ( - + ) : (
{isMobile ? mobileTaskAccordion : taskContent}