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