Вопрос или проблема
Здравствуйте, у меня возникают проблемы с этой функцией, где мне нужно перерисовать родительский компонент, который имеет список аккордеонов. Данные для перерисовки компонента поступают из буферного потока API каждые 200 мс. Я использую SSE (события, отправляемые сервером), чтобы получать данные, потому что веб-сокеты не были вариантом. Моя основная цель — найти способ отрисовать компонент с аккордеонами с использованием буферных данных, не вызывая зависания страницы. Может кто-нибудь помочь мне в этом вопросе?
Метод, который я использую для получения данных из буферного API:
export function useLiveTail(sourceId, filter) {
const [liveTailData, setLiveTailData] = useState([]);
const eventSourceRef = useRef(null);
const recordLimit = 50000;
const closeEventSource = () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
};
useEffect(() => {
if (!sourceId) return;
closeEventSource();
setLiveTailData([])
const queryParams = filter ? `?filter=${encodeURIComponent(filter)}` : '';
eventSourceRef.current = new EventSource(`${baseURL}/v1/livetail/${sourceId}${queryParams}`);
eventSourceRef.current.onmessage = (event) => {
const newData = JSON.parse(event.data);
// liveTailBufferRef.current.push(newData);
setLiveTailData(((prevData) => {
const updatedData = [...prevData, newData];
// Ограничение по количеству записей
if (updatedData.length > recordLimit) {
return updatedData.slice(-recordLimit);
}
return updatedData
}))
};
eventSourceRef.current.onerror = (error) => {
closeEventSource();
};
return () => closeEventSource();
}, [sourceId, filter]);
return { liveTailData , closeEventSource };
}
Мой родительский компонент с аккордеонами:
import React, { useState, useContext, useEffect, useRef } from 'react';
import { Grid, Box, Paper, Accordion, AccordionSummary, AccordionDetails, Typography } from '@mui/material';
import { ExpandMore, FilterAlt } from '@mui/icons-material';
import CustomBreadcrumbs from '../../components/Globals/CustomBreadcrumbs';
import { lightTheme, darkTheme } from '../../globalStyles';
import { AuthContext } from '../../AuthContext';
import Cookies from 'js-cookie';
import CustomInputSelect from '../../components/Globals/CustomInputSelect';
import CustomButton from '../../components/Globals/CustomButton';
import CustomInput from '../../components/Globals/CustomInput';
import { getSourceList } from '../../API/Sources/Sources';
import { mockTaxonamy } from '../../assets/flags/mockData';
import moment from 'moment';
import HoverDescription from './Componentes/HoverDescription';
import { useLiveTail } from '../../API/Live Tail/Live Tail';
// Компонент для определения цвета данных JSON
const getValueColor = (value) => {
if (typeof value === 'number') return 'blue';
if (typeof value === 'string') return 'green';
if (typeof value === 'boolean' || value === null) return 'orange';
return 'black';
};
// Рекурсивная отрисовка JSON с цветом
const renderJsonWithColors = (obj, level = 0) => {
return Object.keys(obj).map((key, index) => {
const value = obj[key];
const valueTypeColor = getValueColor(value);
return (
<Box key={index} display="flex" flexDirection="row" paddingLeft={level * 20}>
<Typography sx={{ color: 'purple', fontWeight: 'bold', marginRight: '8px' }}>
"{key}":
</Typography>
{typeof value === 'object' && value !== null ? (
<Box display="flex" flexDirection="column" marginLeft="8px">
<Typography>{"{"}</Typography>
{renderJsonWithColors(value, level + 1)}
<Typography>{"}"}</Typography>
</Box>
) : (
<Typography sx={{ color: valueTypeColor, marginLeft: '8px' }}>
{typeof value === 'string' ? `"${value}"` : `${value}`}
</Typography>
)}
</Box>
);
});
};
const LiveTail = () => {
const { userData } = useContext(AuthContext);
const theme = Cookies.get(`theme_${userData.email}`);
const breadCrumbsData = [{ label: 'Sources', href: '/sources' }];
// Состояния
const [sourceList, setSourceList] = useState([]);
const [currentSource, setCurrentSource] = useState();
const [auxFilter, setAuxFilter] = useState('');
const [filter, setFilter] = useState('');
const [expanded, setExpanded] = useState(false);
const [hoveredAccordion, setHoveredAccordion] = useState(null);
const [visibleRecords, setVisibleRecords] = useState(5); // Количество записей для первичной загрузки
const paperRef = useRef(null); // Ссылка на контейнер для прокрутки
const { liveTailData, closeEventSource } = useLiveTail(currentSource, filter);
const sortedLiveTailData = liveTailData.sort((a, b) => moment(a.timestamp).diff(moment(b.timestamp)));
// Обработчики
const handleSourceChange = (event) => {
setCurrentSource(event.target.value);
setAuxFilter('');
};
const handleFilterChange = (event) => {
setAuxFilter(event.target.value);
};
const handleAccordionChange = (panel) => (event, isExpanded) => {
setExpanded(isExpanded ? panel : false);
};
const handleMouseEnter = (id) => {
setHoveredAccordion(id);
};
const handleMouseLeave = () => {
setHoveredAccordion(null);
};
const handleSetFilter = () => {
setFilter(auxFilter);
};
const handleCopyContent = (content, event) => {
event.stopPropagation();
navigator.clipboard.writeText(content)
.then(() => {
alert('Содержимое скопировано в буфер обмена');
})
.catch((err) => {
console.error('Не удалось скопировать содержимое: ', err);
});
};
// Обработчик события прокрутки для ленивой загрузки
const handleScroll = () => {
if (paperRef.current) {
const { scrollTop, scrollHeight, clientHeight } = paperRef.current;
if (scrollTop + clientHeight >= scrollHeight - 10) {
// Пользователь достиг нижней части, загрузить больше данных
setVisibleRecords((prev) => Math.min(prev + 20, sortedLiveTailData.length));
}
}
};
// Получение списка источников
useEffect(() => {
const fetchSourceList = async () => {
try {
const aux = await getSourceList();
const newArray = aux.data.data.items.map((item) => ({
label: item.name,
value: item.id,
}));
setSourceList(newArray);
if (newArray.length > 0) {
setCurrentSource(newArray[0].value);
} else {
setCurrentSource(null);
}
} catch (error) {
console.error('Ошибка при получении списка источников:', error);
}
};
fetchSourceList();
}, []);
return (
<div
style={{
backgroundColor: theme === 'light' ? lightTheme.background : darkTheme.background,
height: '100%',
overflow: 'hidden',
}}
>
<Grid container>
<Grid style={{ padding: '0px 20px 20px 0px' }} item xs={12}>
<CustomBreadcrumbs links={breadCrumbsData}></CustomBreadcrumbs>
</Grid>
</Grid>
<Grid container spacing={1} alignItems="center" justifyContent="space-between">
<Grid item xs={3} style={{ alignSelf: 'flex-start' }}>
<CustomInputSelect
name="sourceSelected"
value={currentSource || ''}
options={sourceList}
onChange={handleSourceChange}
fullWidth={true}
disabled={false}
/>
</Grid>
<Grid item xs={6} style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
<div style={{ marginRight: '10px', width: '270px' }}>
<CustomInput
name="filter"
value={auxFilter}
placeholder={'Фильтр'}
variant="outlined"
onChange={handleFilterChange}
fullWidth={true}
disabled={false}
/>
</div>
<CustomButton
background={theme === 'light' ? lightTheme.buttons.primary.background : darkTheme.buttons.primary.background}
color={theme === 'light' ? lightTheme.buttons.primary.text : darkTheme.buttons.primary.text}
hoverColor={theme === 'light' ? lightTheme.buttons.primary.hover : darkTheme.buttons.primary.hover}
text="Фильтр"
action={handleSetFilter}
float={false}
icon={<FilterAlt />}
/>
</Grid>
</Grid>
<Grid container style={{ height: '100%' }}>
<Grid item style={{ height: '100%' }} xs={12}>
<Box sx={{ padding: '16px 0px', width: '100%', height: '100%' }}>
<Grid style={{ height: '100%' }} container>
<Grid style={{ height: '100%' }} item xs={12}>
<Paper
ref={paperRef}
onScroll={handleScroll}
elevation={10}
sx={{ mb: 2, height: '100%', overflowY: 'auto' }}
>
{sortedLiveTailData.slice(0, visibleRecords).map((item, index) => (
<Accordion
key={item.id}
expanded={expanded === `panel${index}`}
onChange={handleAccordionChange(`panel${index}`)}
onMouseEnter={() => handleMouseEnter(item.id)}
onMouseLeave={handleMouseLeave}
>
<AccordionSummary
expandIcon={<ExpandMore />}
aria-controls={`panel${index}-content`}
id={`panel${index}-header`}
sx={{ display: 'flex', alignItems: 'center' }}
>
<HoverDescription
row={item}
taxonomy={mockTaxonamy}
onCopy={handleCopyContent}
hoveredAccordion={hoveredAccordion}
/>
</AccordionSummary>
<AccordionDetails>{renderJsonWithColors(item)}</AccordionDetails>
</Accordion>
))}
</Paper>
</Grid>
</Grid>
</Box>
</Grid>
</Grid>
</div>
);
};
export default LiveTail;
Я уже пробовал ленивую загрузку, но это не сработало, потому что может быть 50000 записей…
Ответ или решение
Ответ на вопрос о повторном рендеринге данных буфера из Streaming API с использованием React
Здравствуйте! Давайте разберем вашу проблему с повторным рендерингом родительского компонента, который содержит список аккордионов, с данными, поступающими из API потоковой передачи, используя SSE.
1. Проблема с производительностью
Вы столкнулись с проблемой многократного и быстрого обновления состояния, что может привести к заеданию или зависанию страницы, особенно при обновлении стейта каждые 200 мс. Также работа с 50,000 записями – это большая нагрузка. Следовательно, необходимо оптимизировать рендеринг.
2. Подходы для оптимизации
a. Использование useMemo
Как вариант, вы можете использовать useMemo
для предотвращения избыточного пересоздания элементов, если данные не изменились. Оберните ваш массив аккордионов в useMemo
, чтобы обновлять его только тогда, когда изменяется sortedLiveTailData
.
const memoizedAccordions = useMemo(() => {
return sortedLiveTailData.slice(0, visibleRecords).map((item, index) => (
<Accordion key={item.id} expanded={expanded === `panel${index}`} onChange={handleAccordionChange(`panel${index}`)}>
<AccordionSummary expandIcon={<ExpandMore />} aria-controls={`panel${index}-content`} id={`panel${index}-header`}>
<HoverDescription row={item} taxonomy={mockTaxonamy} onCopy={handleCopyContent} hoveredAccordion={hoveredAccordion} />
</AccordionSummary>
<AccordionDetails>{renderJsonWithColors(item)}</AccordionDetails>
</Accordion>
));
}, [sortedLiveTailData, visibleRecords, expanded, hoveredAccordion]);
b. Ограничение на количество обновлений
Вы можете реализовать механизм, который будет обновлять ведомые компоненты не каждый раз, когда получаете новые данные, а через определенные промежутки времени. Для этого можно использовать debounce
, что особенно полезно, если данные не настолько важны, чтобы обновлять их мгновенно.
const [debouncedLiveTailData, setDebouncedLiveTailData] = useState([]);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedLiveTailData(liveTailData);
}, 1000); // Задержка в 1 секунду
return () => {
clearTimeout(handler);
};
}, [liveTailData]);
c. Реализация виртуализации списка
Если вы работаете с большим объемом данных (например, 50,000 записей), стоит рассмотреть возможность виртуализации списка. Библиотеки, такие как react-window
или react-virtualized
, помогут отрисовывать только видимые аккордионы и значительно улучшат производительность.
import { FixedSizeList as List } from 'react-window';
...
<List
height={500} // высота списка
itemCount={sortedLiveTailData.length}
itemSize={35} // высота каждого элемента
width={300}
>
{({ index, style }) => (
<div style={style}>
{/* ваш аккордеон */}
<Accordion key={sortedLiveTailData[index].id} expanded={expanded === `panel${index}`} onChange={handleAccordionChange(`panel${index}`)}>
<AccordionSummary expandIcon={<ExpandMore />}>
{/* контент аккордеона */}
</AccordionSummary>
<AccordionDetails>{renderJsonWithColors(sortedLiveTailData[index])}</AccordionDetails>
</Accordion>
</div>
)}
</List>
3. Заключение
Это оптимизации должны помочь вам избежать сбоев страницы и сделать работу с вашим компонентом более плавной, даже при большом количестве данных. Важно помнить о производительности при работе с компонентами и их состоянием в React. Если предложенные решения все же не дают нужного эффекта, вам может понадобиться рассмотреть более сложные паттерны, такие как управление состоянием через библиотеки вроде Redux или MobX, что обеспечит более предсказуемое и управляемое взаимодействие с данными.
Надеюсь, это поможет вам успешно реализовать вашу функциональность! Если у вас есть дополнительные вопросы или нужна помощь, не стесняйтесь обращаться!