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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user