API-интерфейс Intersection Observer переходит в бесконечный цикл рендеринга
Я пытаюсь использовать API наблюдателя пересечения для условного отображения элементов в сетке CSS, когда пользователь начинает прокрутку, но, похоже, происходит бесконечный цикл рендеринга. Вот мой код.
import { Portfolio } from '@/common/types';
import {
Box,
Flex,
Text,
useColorModeValue as mode,
Divider,
Grid,
GridItem,
Center,
Switch,
FormControl,
FormLabel,
Progress,
Stack,
Link,
Circle,
Button,
ButtonGroup,
useToast,
ChakraProps,
} from '@chakra-ui/react';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { useInView } from 'react-intersection-observer';
import { useRef, forwardRef, useCallback } from 'react';
export type PortfolioListItemProps = {
portfolio: Portfolio;
hidden: boolean | undefined;
};
export const PortfolioListItem = ({
portfolio,
hidden,
}: PortfolioListItemProps) => {
const {
symbol,
name,
buy_qty,
sell_qty,
avg_buy_price,
avg_sell_price,
pnl,
exit_date,
entry_date,
isin_no,
} = portfolio;
{
const toast = useToast();
const showCopySuccess = () => {
toast({
description: `Copied to clipboard`,
status: `success`,
duration: 2000,
isClosable: true,
position: `top`,
});
};
let bgColor = `white`;
let color = `gray.500`;
let dateFmtExit = ``;
let dateFmtEntry = ``;
const fmt = new Intl.NumberFormat(`en-IN`, {
style: `currency`,
currency: `INR`,
notation: `compact`,
});
const mo = new Intl.DateTimeFormat(`en-IN`, {
month: `short`,
day: `2-digit`,
year: `numeric`,
});
if (pnl > 0) {
color = `green.400`;
} else if (pnl < 0) {
color = `red.400`;
}
if (exit_date) {
const dt = new Date(exit_date);
if (dt.getFullYear() > 1900) {
dateFmtExit = mo.format(dt);
bgColor = `red.50`;
}
}
if (entry_date) {
const dt = new Date(entry_date);
if (dt.getFullYear() > 1900) {
dateFmtEntry = mo.format(dt);
}
}
return (
<Grid
gridTemplateColumns="1fr 1fr"
gridTemplateRows="min-content"
shadow="md"
rounded={{ md: `lg` }}
bg={bgColor}
hidden={hidden}
>
<GridItem display="flex" colSpan={2}>
<Stack
bg="white"
align="center"
borderTopLeftRadius={6}
borderTop={1}
borderLeft={1}
borderRight={1}
borderStyle="solid"
borderColor="gray.300"
p={4}
w="50%"
>
<Text color="green.400" fontWeight="semibold" fontSize="xl">
{fmt.format(avg_buy_price)}
</Text>
<Text color="gray.400" fontWeight="semibold">
{buy_qty} qty
</Text>
</Stack>
<Stack
bg="white"
align="center"
borderTop={1}
borderRight={1}
borderTopRightRadius={6}
borderStyle="solid"
borderColor="gray.300"
p={4}
w="50%"
>
<Text color="red.400" fontWeight="semibold" fontSize="xl">
{fmt.format(avg_sell_price)}
</Text>
<Text color="gray.400" fontWeight="semibold">
{sell_qty} qty
</Text>
</Stack>
</GridItem>
<GridItem colSpan={2}>
<Stack
spacing={1}
borderTop={1}
borderLeft={1}
borderRight={1}
borderStyle="solid"
borderColor="gray.300"
p={4}
bg={bgColor}
mt="-px"
>
<Text
as="button"
textAlign="left"
fontWeight="bold"
outline="none"
isTruncated
cursor="pointer"
casing="uppercase"
onClick={() => {
navigator?.clipboard?.writeText(name);
showCopySuccess();
}}
>
{name}
</Text>
<Flex justify="space-between">
<Stack spacing={2} align="center" direction="row">
<Text
as="button"
textAlign="left"
cursor="pointer"
outline="none"
onClick={() => {
navigator?.clipboard?.writeText(symbol);
showCopySuccess();
}}
fontSize="sm"
color="gray.400"
fontWeight="semibold"
>
{symbol}
</Text>
<Circle size="6px" bg="gray.300" />
<Text
as="button"
textAlign="left"
cursor="pointer"
outline="none"
onClick={() => {
navigator?.clipboard?.writeText(isin_no);
showCopySuccess();
}}
fontSize="sm"
color="gray.400"
fontWeight="semibold"
>
{isin_no}
</Text>
</Stack>
<Link
href={`https://www.nseindia.com/get-quotes/equity?symbol=${symbol}`}
isExternal
>
<ExternalLinkIcon />
</Link>
</Flex>
<Stack justify="space-between" direction="row">
<Text fontSize="sm" color="gray.400" fontWeight="semibold">
{dateFmtEntry}
</Text>
<Text fontSize="sm" color="gray.400" fontWeight="semibold">
{dateFmtExit}
</Text>
</Stack>
</Stack>
</GridItem>
<GridItem colSpan={2}>
<Flex
spacing="2"
borderTop={1}
borderLeft={1}
borderRight={1}
justify="space-between"
borderStyle="solid"
borderColor="gray.300"
p={4}
align="center"
bg="white"
mt="-px"
borderBottomRightRadius={6}
borderBottomLeftRadius={6}
>
<Text color="gray.400" casing="uppercase" fontWeight="semibold">
Profit & Loss
</Text>
<Text color={color} fontSize="xl" fontWeight="bold">
{fmt.format(pnl)}
</Text>
</Flex>
</GridItem>
</Grid>
);
}
};
export type PortfolioListProps = {
portfolios?: Portfolio[];
title: string;
isLoading: boolean;
headerSlot?: React.ReactElement;
};
export const PortfolioList = ({
portfolios,
title,
isLoading,
headerSlot,
}: PortfolioListProps) => {
const parRef = useRef(null);
const ref = useRef(null);
const [inViewRef, inView] = useInView({
threshold: 1,
rootMargin: '20px',
root: parRef?.current,
});
const setRefs = useCallback(
(node) => {
// Ref's from useRef needs to have the node assigned to `current`
ref.current = node;
// Callback refs, like the one from `useInView`, is a function that takes the node as an argument
inViewRef(node);
},
[inViewRef],
);
return (
<Box
w="100%"
mx="auto"
rounded={{ md: `lg` }}
bg={mode(`white`, `gray.700`)}
shadow="md"
overflow="hidden"
>
<Flex align="center" justify="space-between" px="6" py="4">
<Text as="h3" fontWeight="bold" fontSize="xl">
{title}
</Text>
{headerSlot}
</Flex>
<Progress
size="xs"
w="100%"
colorScheme="blue"
isIndeterminate
hidden={!isLoading}
/>
<Divider />
<Grid
p={4}
gap={4}
templateColumns="1fr 1fr 1fr 1fr"
templateRows="min-content"
maxH="500px"
minH="500px"
overflowY="auto"
id="list"
ref={parRef}
>
{portfolios && portfolios?.length > 0 ? (
portfolios.map((pt) => (
<GridItem ref={setRefs} _last={{ mb: 4 }} key={pt.symbol}>
<PortfolioListItem
hidden={!inView}
key={pt.symbol}
portfolio={pt}
/>
</GridItem>
))
) : (
<GridItem hidden={isLoading} colSpan={4} p={8}>
<Center height="100%">No data found</Center>
</GridItem>
)}
</Grid>
</Box>
);
};
2 ответа
Проблема: та же ссылка на 1000 элементов
У вас 1000
GridItem
компоненты, которые все получают один и тот же обратный вызов ref
setRefs
. Все они имеют одинаковую ценность, даже если мы знаем, что в любой момент времени одни видны, а другие нет. В итоге происходит то, что каждый элемент перезаписывает ранее установленную ссылку, так что все 1000 элементов получают логическое значение
inView
который показывает, находится ли последний элемент в списке в поле зрения, а не сам ли он.
Решение: для каждого элемента
Чтобы знать, находится ли каждый отдельный компонент в поле зрения или нет, нам нужно использовать ловушку отдельно для каждого элемента в списке. Мы можем переместить код для каждого элемента в отдельный компонент. Этому компоненту нужно передать его номер
ix
и варианты
useInView
крючок (мы также могли бы просто передать корневую ссылку и создать
options
объект здесь).
import { Box, Flex, Text, useColorModeValue as mode, Divider, Grid, GridItem, Center } from '@chakra-ui/react';
import { useInView, IntersectionOptions } from 'react-intersection-observer';
import React, { useRef } from 'react';
interface ItemProps {
ix: number;
inViewOptions: IntersectionOptions;
}
export const ListItem = ({ix, inViewOptions}: ItemProps) => {
const {ref, inView}= useInView(inViewOptions);
return (
<GridItem bg={inView?"red.100":"blue.100"} ref={ref} _last={{ mb: 4 }} key={ix}>
<Center border={1} borderColor="gray.100" borderStyle="solid" h={16} w="100%">
Item {ix}
</Center>
</GridItem>
)
}
export type PortfolioListProps = {
title: string;
};
export const PortfolioList = ({
title,
}: PortfolioListProps) => {
const parRef = useRef(null);
return (
<Box
w="100%"
mx="auto"
rounded={{ md: `lg` }}
bg={mode(`white`, `gray.700`)}
shadow="md"
overflow="hidden"
>
<Flex align="center" justify="space-between" px="6" py="4">
<Text as="h3" fontWeight="bold" fontSize="xl">
{title}
</Text>
</Flex>
<Divider />
<Grid
p={4}
gap={4}
templateColumns="1fr 1fr 1fr 1fr"
templateRows="min-content"
maxH="500px"
minH="500px"
overflowY="auto"
id="list"
ref={parRef}
>
{[...Array(1000)].map((pt,ix) => (
<ListItem ix={ix} key={ix} inViewOptions={{
threshold: 1,
rootMargin: '0px',
root: parRef?.current,
}}/>
))}
</Grid>
</Box>
);
};
Компонент работает не так, как вы ожидаете, потому что вы используете одну и ту же ссылку для всех элементов. Вы можете использовать
ref
для хранения массива ссылок или создания компонента с логикой элемента списка.
Если вы не хотите отображать все элементы одновременно, вы можете визуализировать часть (100), и каждый раз
scroll
доходит до конца, рендер еще 100 и так далее. Я рекомендую вам использовать
React.memo
чтобы не отображать элемент каждый раз при обновлении состояния:
PortfolioItem.js
const PortfolioItem = React.memo(({ ix }) => {
const ref = useRef();
const [inViewRef, inView] = useInView({
threshold: 1,
rootMargin: '0px',
});
const setRefs = useCallback(
(node) => {
ref.current = node;
inViewRef(node);
},
[], //--> empty dependencies
);
return ( <GridItem bg={inView?"red.100":"blue.100"} ref={setRefs} _last={{ mb: 4 }} >
<Center border={1} borderColor="gray.100" borderStyle="solid" h={16} w="100%">
Item {ix}
</Center>
</GridItem>)
});
PortfolioList.js
export const PortfolioList = ({
title,
count = 100
}: PortfolioListProps) => {
const ref = useRef(null);
const items = [...Array(1000)];
const [index, setIndex] = useState(count);
useEffect(()=> {
const grid = ref.current;
function onScroll(){
if(grid.offsetHeight + grid.scrollTop >= grid.scrollHeight) {
setIndex(prev => prev+count);
}
}
grid.addEventListener("scroll", onScroll);
return ()=> {
grid.removeEventListener("scroll", onScroll);
}
}, []);
return (
<Box
w="100%"
mx="auto"
rounded={{ md: `lg` }}
bg={mode(`white`, `gray.700`)}
shadow="md"
overflow="hidden"
>
<Flex align="center" justify="space-between" px="6" py="4">
<Text as="h3" fontWeight="bold" fontSize="xl">
{title}
</Text>
</Flex>
<Divider />
<Grid
p={4}
gap={4}
templateColumns="1fr 1fr 1fr 1fr"
templateRows="min-content"
maxH="500px"
minH="500px"
overflowY="auto"
id="list"
ref={ref}
>
{items.slice(0,index).map((pt,ix) => (
<PortfolioItem ix={ix} key={`Postfolio__item-${ix}`}/>
))
}
</Grid>
</Box>
);
};