Привет друг ! В этой статье попробуем реализовать универсальный компонент (обёртку), для реализации загрузки новых данных при достижении конца списка пользователем 🧐.
За достижением оного обратимся за помощью к браузерному API - Intersection Observer.
Чего мы хотим добиться? тут 🥸.
И так - поехали !🛼
1. Создадим моковые данные и фейковый запрос:
В родительском компоненте, для нашего будущего InfiniteScrollWrapper.tsx - тот самый универсальный компонент пишем:
// моковый массив
const MOCK_DATA = Array.from({ length: 100 }, (_, i) => `Item ${i + 1}`);
// колличество эллементв на странницу
const PAGE_SIZE = 10;
// наш массив MOCK_DATA
const [data, setData] = useState<string[]>(MOCK_DATA.slice(0, PAGE_SIZE));
// состояние для "странницы"
const [page, setPage] = useState<number>(1);
// флажок надо еще или нет :)
const [hasMore, setHasMore] = useState<boolean>(true);
// фейковый запрос
const loadMore = async (): Promise<string[]> => {
return new Promise((resolve) => {
setTimeout(() => {
const nextPageData = MOCK_DATA.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
setData((prevData) => [...prevData, ...nextPageData]);
setPage((prevPage) => prevPage + 1);
if ((page + 1) * PAGE_SIZE >= MOCK_DATA.length) {
setHasMore(false);
}
resolve(nextPageData);
}, 1000);
});
};
2. Создадим универсальный InfiniteScrollWrapper.tsx:
При типизации функции запроса loadMore, предпочёл дженерик <T>, вместо any, мне кажется так юридически правильнее будет 🙃.
import {ReactNode, useEffect, useRef} from 'react';
interface IScrollCheckWrapper<T> {
children: ReactNode;
// Типизированный дженерик для возврата данных
loadMore: () => Promise<T>;
// Флаг, показывающий, есть ли еще данные для загрузки
hasMore: boolean;
}
export const InfiniteScrollWrapper = <T,>({ children, loadMore, hasMore}: IScrollCheckWrapper<T>) => {
const observerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
// Сохраняем текущее значение ref
const currentRef = observerRef.current;
// вот и IntersectionObserver подъехал :)
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
loadMore();
}
});
if (currentRef) {
observer.observe(currentRef);
}
return () => {
if (currentRef) {
observer.unobserve(currentRef);
}
};
}, [loadMore, hasMore]);
return (
<div>
{children}
<div ref={observerRef} style={{ height: '1px', background: 'transparent' }} />
</div>
);
};
3. Допилим родителя:
App.tsx:
return (
<div style={{ minHeight: '100vh', padding: '20px' }}>
<InfiniteScrollWrapper<string[]>
loadMore={loadMore}
hasMore={hasMore}
>
<ul style={{display: 'flex', flexDirection: 'column', gap: '6px'}}>
{data.map((item, index) => (
<li key={index} style={{
listStyle: 'none',
color: 'black',
padding: '20px',
borderRadius: '12px',
border: '1px solid black',
width: '300px',
height: '30px',
}}>{item}</li>
))}
</ul>
</InfiniteScrollWrapper>
</div>
);
4. Чего-то не хватает 🤔?
В функции loadMore, не просто так стоит задержка в 1 секунду. Таким образом эмитируем "pending" нашего запроса. Ну и как порядочные разрабы вешаем лоадер.
В App.tsx пишем передаём доп пропс loader:
<InfiniteScrollWrapper<string[]>
loadMore={loadMore}
hasMore={hasMore}
loader={<div style={{color: 'black'}}>Загрузка...</div>}
>
В InfiniteScrollWrapper.tsx принимаем его, с распростёртыми объятиями - разумеется 🤗.
interface IScrollCheckWrapper<T> {
children: ReactNode;
loadMore: () => Promise<T>;
hasMore: boolean;
// Опциональный лоадер
loader?: ReactNode;
}
Ну и привяжем его отрисовку ко флагу, который сигнализирует о том, что надо ещё элементов, на странницу, подкинуть (hasMore).
{hasMore && loader && <div style={{ textAlign: 'center', margin: '10px 0' }}>{loader}</div>}
5. Итоговый код:
//--------App.tsx:
import {useState} from 'react';
import {InfiniteScrollWrapper} from './components/InfiniteScrollWrapper.tsx';
import './App.css';
export const App = () => {
const MOCK_DATA = Array.from({ length: 100 }, (_, i) => `Item ${i + 1}`);
const PAGE_SIZE = 10;
const [data, setData] = useState<string[]>(MOCK_DATA.slice(0, PAGE_SIZE));
const [page, setPage] = useState<number>(1);
const [hasMore, setHasMore] = useState<boolean>(true);
const loadMore = async (): Promise<string[]> => {
return new Promise((resolve) => {
setTimeout(() => {
const nextPageData = MOCK_DATA.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
setData((prevData) => [...prevData, ...nextPageData]);
setPage((prevPage) => prevPage + 1);
if ((page + 1) * PAGE_SIZE >= MOCK_DATA.length) {
setHasMore(false);
}
resolve(nextPageData);
}, 1000);
});
};
return (
<div style={{ minHeight: '100vh', padding: '20px' }}>
<InfiniteScrollWrapper<string[]>
loadMore={loadMore}
hasMore={hasMore}
loader={<div style={{color: 'black'}}>Загрузка...</div>}
>
<ul style={{display: 'flex', flexDirection: 'column', gap: '6px'}}>
{data.map((item, index) => (
<li key={index} style={{
listStyle: 'none',
color: 'black',
padding: '20px',
borderRadius: '12px',
border: '1px solid black',
width: '300px',
height: '30px',
}}>{item}</li>
))}
</ul>
</InfiniteScrollWrapper>
{!hasMore && <p style={{color: 'black'}}>Все эллементы загружены!</p>}
</div>
);
};
//--------InfiniteScrollWrapper.tsx:
import {ReactNode, useEffect, useRef} from 'react';
interface IScrollCheckWrapper<T> {
children: ReactNode;
loadMore: () => Promise<T>;
hasMore: boolean;
loader?: ReactNode;
}
export const InfiniteScrollWrapper = <T,>({
children,
loadMore,
hasMore,
loader,
}: IScrollCheckWrapper<T>) => {
const observerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const currentRef = observerRef.current; // Сохраняем текущее значение ref
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
loadMore();
}
});
if (currentRef) {
observer.observe(currentRef);
}
return () => {
if (currentRef) {
observer.unobserve(currentRef);
}
};
}, [loadMore, hasMore]);
return (
<div>
{children}
{hasMore && loader && <div style={{ textAlign: 'center', margin: '10px 0' }}>{loader}</div>}
<div ref={observerRef} style={{ height: '1px', background: 'transparent' }} />
</div>
);
};
Теперь мы можем использовать InfiniteScrollWrapper в любом месте нашего приложения! Да и вообще мы молодцы 🧸!
Спасибо за внимание! До Новых Встреч!🤗🤗🤗