У меня есть приложение-реактор, которое отображает сетку из трех столбцов. Я пытаюсь реализовать API Intersection Observer для включения бесконечной прокрутки, однако он не работает. Выводится следующий код:
in handleObserver
TestStateUpdater2.tsx:168 this is data undefined
и мой текущий код:
import { Container } from "pixi.js";
import React, { useState, useRef, useEffect } from "react";
export function TestStateUpdater2() {
const [data, setData] = useState<Array<ICarRequest>>()
const ref = useRef(null);
useEffect(()=> {
setData([
{
priceMax: 1,
newOrUsed:"new",
make: "make",
model: "model",
year: 222,
bodyType: "jeep",
fuelType: "gas",
color: "blue",
thumbnail: "picture",
trim: "nice",
dealerPrice: "333",
msrp: 345,
driveTrain: "5"
},
...
])
}, [])
useEffect(() => {
const observer = new IntersectionObserver(
handleObserver,
{
threshold: []
}
);
if (ref && ref.current){
observer.observe(ref.current);
}
return () => {
if (ref.current) {
observer.unobserve(ref.current);
}
}
}, []);
function handleObserver(x: IntersectionObserverEntry[]) {
console.info("in handleObserver")
console.info("this is data ", data)
const newArray = [
{
priceMax: 1,
newOrUsed:"new",
make: "make",
model: "model",
year: 222,
bodyType: "jeep",
fuelType: "gas",
color: "blue",
thumbnail: "picture",
trim: "nice",
dealerPrice: "333",
msrp: 345,
driveTrain: "5"
}
];
// if (data)
// setData([...data,...newArray]);
}
function getData(){
if (data){
const somedata = data.slice(0,10).map(x=>
<div>
<Card>
<Card.Img variant = "top" src = {x.thumbnail} />
<Card.Body>
<Card.Title>{x.year} {x.make} {x.model} {x.trim}</Card.Title>
<ListGroup>
<ListGroup.Item key = {x.make + x.model + x.year + x.dealerPrice}>Dealer price: ${x.dealerPrice}</ListGroup.Item>
<ListGroup.Item key = {x.make + x.model + x.year + x.msrp}>MSRP: ${x.msrp}</ListGroup.Item>
<ListGroup.Item key = {x.make + x.model + x.year + x.driveTrain}>Drive Train: {x.driveTrain}</ListGroup.Item>
</ListGroup>
</Card.Body>
</Card>
</div>
)
return somedata
}
}
return (
<Container className = "container">
<Container className = "theContainer">
<div id = "carDisplay" ref = {ref} className = "row row-cols-3">
{getData()}
</div>
</Container>
<Container className = "theContainer2">
<div ref = {ref} className = "footer">
This is a footer
</div>
</Container>
</Container>
)
}
Когда я прокручиваю, срабатывает Observer, но в состоянии undefined
. Я хочу, чтобы сработал метод handleObserver, где я планирую сделать еще один вызов API для добавления к «данным». Я не понимаю, почему «данные» undefined
в обратном вызове?
Если я изменю useEffect, который создает наблюдателя, на:
useEffect(() => {
const observer = new IntersectionObserver(
handleObserver,
...
}, [data]);
Теперь «данные» в handleObserver имеют значения и больше не являются undefined
. Но если я продолжу и обновлю там «данные»:
if (data) {
setData([...data,...newArray]);
}
Это запускает бесконечный цикл.
Я не уверен, что мне делать? Я хочу прокрутить страницу вниз, заставить Observer вызвать обратный вызов handleObserver, а затем загрузить больше элементов в мою сетку.
Стоит отметить, что обратный вызов handleObserver
по своей сути требует доступа к setData
из области действия компонента. Если он его экстернализует, ничего особенного не изменится, поскольку тогда ему нужно будет просто создать новую оболочку обратного вызова, которая сопоставляет область действия компонента с недавно экстернализованным fn. С точки зрения организации кода, ему, вероятно, нужен здесь специальный хук, поскольку от концептуальной связи обработчика пересечения с состоянием никуда не деться. Но в любом случае, это чистка.
Причина, по которой data
равна undefined
(до того, как вы добавили data
в массив deps), заключается в том, что эффект захватывал handleObserver
из первоначального рендеринга, который «закрывается» независимо от значений, которые были во время этого первоначального рендеринга.
По сути, эффект, регистрирующий IntersectionObserver
, запускался только один раз при монтировании компонента и использовал тот handleObserver
, который у него был на тот момент. Однако этот экземпляр handleObserver
всегда будет ссылаться на состояние исходного рендеринга. Когда data
меняется, IntersectionObserver
все еще сохраняет то же самое handleObserver
, что и при первоначальном рендеринге. И это не приведет к волшебному появлению ссылок на новые ценности. Это потому, что он не обновляет один и тот же объект каждый раз, когда вы это делаете setData
. Создается новый. Итак, у вас устаревшие ссылки.
Когда вы затем добавляете data
в массив deps, внезапно IntersectionObserver
удаляется при изменении data
, и создается новый со свежим handleObserver
, который будет иметь ссылки на новое состояние. Обычно это правильно: от useEffect
не должно отсутствовать никаких зависимостей, а data
здесь действительно является зависимостью, хотя и косвенно.
Это всего лишь запах кода, а не первопричина (читайте дальше), но концептуальные зависимости становятся намного понятнее, если все используемое внутри эффекта извне, что принципиально может изменить свою ссылку между рендерами, ссылается на deps. Правила линта обеспечивают такое поведение. Обычно в итоге у вас получится что-то вроде:
const handleObserver = useCallback(
(x: IntersectionObserverEntry[]) => {
console.info("in handleObserver");
console.info("this is data ", data);
const newArray = [
{
priceMax: 1,
newOrUsed: "new",
make: "make",
model: "model",
year: 222,
bodyType: "jeep",
fuelType: "gas",
color: "blue",
thumbnail: "picture",
trim: "nice",
dealerPrice: "333",
msrp: 345,
driveTrain: "5",
},
];
// if (data)
// setData([...data,...newArray]);
},
[data],
);
useEffect(() => {
const observer = new IntersectionObserver(handleObserver, {
threshold: [],
});
if (ref && ref.current) {
observer.observe(ref.current);
}
return () => {
if (ref.current) {
observer.unobserve(ref.current);
}
};
}, [handleObserver]);
Что более понятно. Обратный вызов handleObserver
должен обновляться при изменении data
, поскольку он ссылается на него. И эффект необходимо повторно запустить при изменении handleObserver
, поскольку он ссылается на него. На самом деле делать это необязательно. Если только вы не хотите странных устаревших ошибок в ссылках, подобных которым вы столкнулись.
Теперь вернемся к вашей проблеме. Когда вы создаете экземпляр IntersectionObserver
, его поведение заключается в том, что обратный вызов (handleObserver
для вас) срабатывает мгновенно.
Это нормально, но вам следует избегать вызова setData()
в этом обратном вызове, когда вам это действительно не нужно. В противном случае вы попадете в цикл, потому что вызов setData()
приведет к изменению data
при следующем повторном рендеринге, что в конечном итоге приведет к повторной регистрации IntersectionObserver
и срабатыванию, так что setData()
вызывается снова. Вы можете увидеть, как это закончится циклом.
На практике это означает, что вам нужно проверить, содержит ли data
данные, которые вы предлагаете добавить, прежде чем сделать это. Когда вы перейдете на асинхронность, это будет включать в себя проверку того, находится ли уже в работе соответствующий запрос API. Вероятно, для этого потребуется поддерживающая структура, которая будет удерживать состояние загрузки для различных триггеров, чтобы ее можно было проверить здесь.
Если он уже есть, или есть запрос в работе, вы не позвоните setData
. Это защищает от проблемы цикла и в любом случае будет необходимо, когда вы перейдете в асинхронное состояние.
В дополнение к этому вы должны использовать форму обратного звонка setState
здесь. Так что setData([...data,...newArray]);
должно быть setData(data => [...data,...newArray]);
. См. документацию.
Кстати, как вы можете заметить, сопоставление API-интерфейсов, основанных на событиях, с примитивами React является сложной задачей. Обычно есть библиотеки, которые сделают все более эргономичным. Например, реакция-пересечение-наблюдатель. Такие вещи уже справляются с этими сложностями, которые являются общими.
Это отличный ответ, спасибо. В итоге я использовал React-Intersection-Observer, и он заработал.
У вас будет гораздо более чистый компонент, если вы переместите все эти встроенные функции за пределы компонента и просто заставите их принимать все необходимые аргументы, чтобы делать все, что им нужно. Нет смысла постоянно определять их, а затем снова выбрасывать каждый раз, когда ваш компонент визуализируется.