Реакт - это библиотека для создания пользовательских интерфейсов. То есть это означает, что мы можем писать интерфейсы на нём не только для браузера, но и для мобилок, так как он использует свой виртуальный DOM.
Мы имеем две основные концепции сайтов:
MPA (Multi Page Application) - сайт состоит из нескольких страниц, при переходе на которые мы подгружаем их данные целиком
SPA (Single Page Application) - весь сайт располагается на одной странице и при переходе на другую страницу в нём меняются только конкретные данные
Реакт основан на компонентном подходе, когда страница строится из отдельных кирпичиков, которые мы можем повторно использовать. Так же он позволяет сосредоточиться на написании логики приложения без работы со слушателями событий, непосредственной работы с DOM (querySelector и подобные операции) - реакт берёт эту работу на себя.
Во время своей работы реакт строит два своих дерева и переносят изменения на конечное третье:
Первое - дерево элементов реакта - когда в нём происходят изменения, они попадают на второе дерево, между которыми происходит сравнение
Второе - виртуальное дерево для сравнения
Третье - это конечный DOM браузера, в которое и вносятся изменения после сравнения (фаза рендеринга, за которую отвечает React DOM или React Native)
Механизм согласования (Reconciliation) осуществляет сравнение элементов дерева реакта.
Так же реакт делит операции по приоритетности и более приоритеные задачи он выполняет быстрее.
11:40 ➝ Начало разработки. Создание проекта
Создание реакт-приложения
Запуск компиляции приложения
Начальной страницей, которая запускает весь рендер приложения является index.js, который рендерится в root диве index.html документа
public > index.html
src > index.js
16:10 ➝ Что такое JSX?
JSX - это препроцессор, который babel переводит в обычный JS
18:11 ➝ Компонент App. Работа с состоянием. UseState
Задача: нам нужно сделать счётчик, который при нажатии на кнопку будет увеличивать значение.
В примере ниже Реакт не понимает, что нужно обновлять значение в определённом компоненте, так как мы подобной функцией отправляем изменение значения в JS (clg покажет, что значение меняется внутри JS), а не в дерево Реакта.
Нам нужно будет вызвать в реакте перерендер нужного нам значения на странице.
Хук useState() возвращает значение с состоянием и функцию для его обновления.
Во время первоначального рендеринга возвращаемое состояние (state) совпадает со значением, переданным в качестве первого аргумента (initialState).
Функция setState используется для обновления состояния. Она принимает новое значение состояния и ставит в очередь повторный рендер компонента.
При увеличении значения счётчика, число увеличивается, а при уменьшении - уменьшается.
22:25 ➝ Управляемый инпут
Управляемый компонент - это компонент, значение которого мы можем изменить, изменив состояние
Мы связали состояние <h1> с тем, что находится в инпуте
24:07 ➝ Первый функциональный компонент
Компоненты мы создаём в папке components
Файл компонента и функция компонента всегда именуются в PascalCase
Компоненты всегда должны возвращать JSX.Element
И теперь данный функциональный компонент <Button> можно использовать в любом месте проекта. Этих компонентов можно навставлять сколько угодно и они будут независимыми друг от друга
26:40 ➝ Первый классовый компонент
Классовый компонент использует внутри себя методы
И так выглядит каунтер:
Сокращение путей импортов до компонентов
Так же ну нужно забывать, что при создании компонента в папке components, мы можем экспортировать удобно эти компоненты из папки, чтобы использовать в других папках (pages или page-components)
components / index.ts
После подобного экспорта, мы сможем получать доступ к данным компонентам, просто обратившись через: import { нужный_компонент } from './components'
30:25 ➝ Что такое хуки? useState, useEffect
Хук - это функция, которую предоставляет React для использования в функциональных компонентах или в своих собственных хуках
Хуки используются только на верхнем уровне вложенности. Их нельзя вкладывать в условия, циклы и другие функции.
31:10 ➝ Стили. CSS. Классы
Для наименования классов в React используют атрибут className, так как слово class уже зарезервировано под классы.
Мы можем просто именовать классы стилей наших элементов стандартным способом
А можем использовать модули для описания стилей. В этом случае нужно:
В названии файла стилей указать .module
В className указать класс через точку от импортированных стилей
Так же стили можно указывать через атрибут style, внутрь которого мы передаём объект со стилями
34:30 ➝ Props. Аргументы компонента.
Свойства, которые мы передаём в компонент, называются props
В рамках React при использовании его вместе с TS мы обязаны использовать интерфейсы для наших получаемых пропсов (чтобы всегда понимать, что компонент получает).
Интерфейсы для компонентов мы обычно расширяем с помощью DetailedHTMLProps и уточняем, какие атрибуты он должен принимать через HTMLAttributes.
В данном случае, мы расширяем компонент от дива, чтобы была возможность в него передать className.
Так же мы указываем children с типом ReactNode - это те данные, которые мы вкладываем между открывающим и закрывающим тегом
PostItem.props.ts
Вместо принимаемого объекта десутруктуризации { language, children } мы бы могли просто написать props, но вытащить сразу нужные значения - это самый оптимальный способ взаимодействия, чтобы сразу видеть получаемые параметры
Так же, чтобы передать сразу все остальные пропсы, которые мы вложим в компонент, можно использовать ...props при получении props и в самом JSX указать, что мы выкладываем все пропсы в этот элемент: <div {...props}>. Такой подход более актуален, когда мы создаём свои компоненты кнопок, инпутов и остальных простых элементов.
PostItem.tsx
Передаются пропсы ровно таким же образом, как и атрибуты. В случае с TS компилятор нам подскажет, какие значения мы можем вставить в данный элемент компонент
PostList.tsx
Так же мы можем вывести props в консоль и увидим, что это просто объект с данными, которые мы передали в компонент извне
36:55 ➝ Работы со списками. Преобразование массива объектов в массив React элементов
Когда нам нужно вывести массив элементов с определённой структурой, мы можем воспользоваться функциями JS внутри JSX. Для этого функции нужно вписать внутрь { } скобок.
Для перебора массива можно воспользоваться функцией map().
Когда мы создаём списки, обязательно для всех элементов нужно указать уникальный ключ и передать его через атрибут key. Для ключа обычно не стоит использовать индекс элемента в массиве - это плохая практика, так как он может поменяться после изменения размера массива. Рекомендуется использовать какой-либо статичный индекс. Это поможет реакту запомнить элемент массива и не перерисовывать все выведенные элементы.
PostList.tsx
И примерно так мы получим итоговый массив наших элементов
42:30 ➝ Создание UI библиотеки. Первые компоненты. CSS модули. Пропс children
Обычно в своей работе придётся часто создавать свою UI-библиотеку под каждый сайт, который мы будем реализовывать.
Компонент кнопки:
Button.tsx
Тут мы опишем те параметры, которые должна принимать в себя кнопка. Чтобы описать получаемые атрибуты, мы должны расширить интерфейс от DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, где мы расширяемся от Button.
Button.props.ts
Стили кнопки:
Button.module.css
50:00 ➝ Предотвращаем обновление страницы при submit формы
Чтобы предотвратить срабатывание дефолтной перезагрузки страницы, нужно использовать на ивенте данного элемента отключение поведения браузера preventDefault()
50:45 ➝ хук useRef. Доступ к DOM элементу. Неуправляемый компонент
Ниже приведены примеры управляемого и неуправляемого компонента:
Управляемый:
Управляемый компонент имеет подконтрольное значение
Это состояние связано с компонентом через значение
Неуправляемый компонент:
Не имеет подконтрольного значения
Для получения доступа к нему используется отдельный хук useRef
Порядок использования рефа:
Инициализируем useRef
Передаём в атриьбут ref проинициализированный useRef
Оборачиваем сам компонент в своей внутренней реализации в forwardRef
Прокидываем ref внутрь реализации компонента
Далее сам компонент <Input> нужно обернуть в forwardRef полностью (всю функцию обернуть внутрь ( ) скобок) и передать внутрь компонента дополнительное свойство ref. Само свойство ref нужно вложить в качестве атрибута внутрь нашего компонента
Input.tsx
И по итогу, мы сможем увидеть в консоли, что реф даёт нам доступ к значению данного инпута
И поэтому вернём обратно обычное взаимодействие с деревом. Однако тут сохраняется проблема в том, что мы используем два разных useState, хотя могли бы сократить запись
Тут представлена более лаконичная запись с использованием одного useState и сокращённой функцией addNewPost
React DevTools - необходимый плагин в работе, который позволит просмотреть дерево элементов страницы, влияние изменения стейта компонентов
59:15 ➝ Обмен данными между компонентами. От родителя к ребенку. От ребенка к родителю.
Мы можем передавать функции четырьмя разными способами:
Самый простой стандартный - это от родителя к ребёнку
От ребёнка к родителю выполняется через callback-фукнцию
Между дочерними компонентами (через родительский)
И глобально в различные компоненты проекта (зачастую через контекст)
Первым делом, нужно выделить форму добавления нового поста в отдельный компонент. И тут нам понадобится реализовать передачу пропсов от дочернего элемента к родительскому.
Передаём через create={createPost} функцию от родительского элемента к дочерней форме на добавление поста.
И передаём функцию remove={removePost} для удаления поста, но уже непосредственно в айтем поста
PostList.tsx
Тут представлен интерфейс поста в отдельном файле
PostList.interface.ts
Сейчас нужно разбить логику так, чтобы можно было передать внутрь дочернего компонента функцию от родительского компонента
PostForm.props.ts
Уже сам отдельный элемент будет в себя принимать функцию удаления поста по полученному посту и сам пост
PostItem.props.ts
Далее тут вызваем функцию из родительского компонента create() в дочернем элементе
PostForm.tsx
Далее переходим в элемент отдельного поста. Тут кнопкой вызываем функцию удаления поста, передавая в неё полный пост: () => remove(post)
PostItem.tsx
Новые посты всё так же добавляются!
Удаление поста так же работает:
01:04:20 ➝ Отрисовка по условию
Отрисовка по условию выполняется крайне просто - через тернарный оператор:
PostList.tsx
При удалении всех постов, у нас вылезет надпись:
01:05:30 ➝ Сортировка. Выпадающий список
Первым делом, нужно реализовать компонент сортировки. Он будет в себя принимать массив опций, дефолтную опцию и определённое выбранное значение пользователем
Select.props.ts
Компонент селекта выводит массив переданных в него опций и триггерит переданную в него функцию onChange при выборе определённого селекта
Select.tsx
В главном компоненте мы создали функцию sortPosts, которую и передаём в дочерний компонент селекта. Внутри функции мы устанавливаем тип селекшена setSelectedSort и производим сравнение списков через localeCompare.
Первым параметром хук принимает в себя функцию, которая высчитывает определённое значение. Вторым параметром принимает в себя массив зависимостей. Если какая-либо из зависимостей изменилась, то хук заново пересчитывает значение.
Если массив зависимостей не был передан, новое значение будет вычисляться при каждом рендере.
Эта оптимизация помогает избежать дорогостоящих вычислений при каждом рендере.
Нужно помнить, что функция, переданная useMemo, запускается во время рендеринга. Не нужно делать там ничего, что обычно не делается во время рендеринга. Например, все побочные эффекты должен выполнять useEffect, а не useMemo.
example:
Далее структура проекта была немного реорганизована и теперь главным компонентом будет компонен страницы Posts.tsx
В самом компоненте Posts.tsx мы создадим общее состояние под поиск, где filter будет отвечать за строку запроса query и компонент селекта sort.
Далее используется две функции sortPosts и sortedAndSearchedPosts, которые фильтруют массив (первый по селекту, второй по запросу) и кешируют эту сортировку. Функция sortPosts удалена, так как её заменяют две вышеописанные функции.
Далее мы вызваем три компонента: форму, фильтр и генерацию списка постов.
page-components / Posts.tsx
Далее идёт компонент PostList, который принимает в себя функцию для удаления поста и массив постов.
components / PostList / PostList.props.ts
Тут выводится список постов, в каждый из которых передаётся своя функция для их удаления
components / PostList / PostList.tsx
Далее идёт фильтр постов, который в себя принимает пропс фильтра по интерфейсу IFilter и функцию setFilter, которая устанавливает новый фильтр.
Сам интерфейс IFilter представляет из себя интерфейс стейта фильтра из главного компонента
components / PostFilter / PostFilter.props.ts
Данный компонент выполняет фильтрацию по инпуту и по селекту
Чтобы добавить ещё один стиль или несколько стилей, можно воспользоваться такой конструкцией: [массив_классов].join(' '), где мы объединяем весь массив через функцию join()
Если нам нужно добавить класс по условию, то можно создать такую проверку:
Компонент модального окна принимает в себя дочерние элементы, состояние видимости и функцию установки этой видимости
components / Modal / Modal.props.ts
Тут уже представлен компонент Modal, который и реализует под собой открытие модального окна по состоянию, переданному от родителя.
Так же тут показано, как можно воспользоваться функцией cn() из внешнего модуля classnames, которая упростит взаимодействие с навешиванием классов на элементы.
Тут нужно сказать, что функция e.stopPropagation() (которая вложена в дочерний элемент обёртки) предотвращает выполнение остальных функций, срабатывание которых повешено на данный элемент или его родителей. Конкретно в данном случае, он не даёт закрыться модальному окну при клике на него (функция закрытия работает не только на родителе изначально, но и на ребёнке)
components / Modal / Modal.tsx
В родительский компонент нужно добавить состояние, которое будет контролировать видимость модального окна.
Далее в функцию createPost() добавим закрытие модального окна при добавлении нового поста
В рендере передаём дополнительную кнопку, которая будет проявлять модальное окно
Внутрь модалки переложим форму для создания нового поста
page-components / Posts.tsx
Итог:
01:30:23 ➝ Анимации. React transition group
Одним из способов добавить анимацию в реакт является react-transition-group, который мы можем использовать как компоненты реакта
Тут нужно обернуть группу элементов в TransitionGroup а каждый отдельный элемент в CSSTransition (в этот же элемент нужно передать свойство ключа, так как он располагается выше, чем PostItem)
components / PostList / PostList.tsx
Стили компонента:
CSSTransition мы наименовали как post и поэтому в определённых состояниях нужно наименовать начальный элемент как post
01:33:40 ➝ Декомпозиция. Кастомные хуки
Когда наш компонент начинает хранить большое количество логики, нужно его декомпозировать и выделять его функции в хелперы или даже отдельные хуки.
Тут будут храниться отдельные хуки, которые мы выцепили из основного компонента создания постов.
hooks / usePosts.ts
В самом компоненте мы просто вызваем сортировку постов
page-components / Posts.tsx
01:36:20 ➝ Работа с сервером. Axios
Первым делом, установим модуль по работе с сервером axios, который позволит просто отправлять запросы на получение данных на сервер
01:38:40 ➝ Жизненный цикл компонента. useEffect
Жизненный цикл компонента React делится на 4 части:
Инициализация (компонент получает пропсы и состояния)
И тут мы подходим к использованию хука useEffect, который позволит выполнять действия под определённые стадии монтировки компонента
В главном компоненте получим массив постов с сервера через fetchPosts и сохраним его в стейт наших постов. Вызвана эта функция будет через useEffect, который в представленном сетапе будет выполнять действие ровно один раз - при загрузке компонента
page-components / Posts.tsx
Итог: мы получили посты с сервера и сразу их отрендерили, так как через useEffect вызвали функцию получения постов
01:43:08 ➝ API. PostService
Чтобы упростить свою работу с сервером через внесение дополнительной абстракции, можно сделать отдельное API на фронте, которое будет получать наши посты с сервера.
Создадим класс со статичной функцией и будем уже использовать эту конструкцию для работы с сервером
Заранее стоит сказать, что обрабатывать ошибку тут - плохой подход, поэтому переловим ошибку в другом месте кода →
API / post.service.ts
И теперь мы можем получить посты более лаконичным и понятным способом
page-components / Posts.tsx
01:44:45 ➝ Индикация загрузки данных с сервера
Мы создали состояние isPostLoading, которое будет отвечать за состояние загрузки постов. В функции fetchPosts добавим изменение этого состояния (так как через него идёт получение данных с сервера). И далее в render укажем, что отображать сообщение о загрузке нужно пока его состояние не перейдёт в false
page-components / Posts.tsx
Итог:
01:46:20 ➝ Компонент Loader. Анимации
Компонент лоадера будет представлять из себя обычный <div>
components / Loader / Loader.tsx
А анимация будет бесконечной и реализованной через обычный CSS
components / Loader / Loader.module.SCSS
Теперь останется только вставить крутилку на страницу
page-components / Posts.tsx
Итог: получена крутилка, которая оповещает о загрузке
Далее нам нужно сделать хук, который будет получать функцию и выполнять её, а так же будет контролировать состояние спиннера загрузки и перехватывать ошибку, если таковая придёт на страницу
hooks / useFetching.ts
И теперь в родительском компоненте используем хук useFetching, который позволит нам убрать отслеживание состояния загрузки внутри родительского компонента
Так же сделаем вывод ошибки, если таковая будет иметься через postsError
page-components / Posts.tsx
Если мы не сможем получить посты с сервера, то увидим вот такую ошибку:
Выводить сразу на одной странице 100 постов - это не самая лучшая идея. Если данные посты будут иметь ещё и фотографии, то загрузка страницы будет долгой и устройство пользователя так же будет сильно нагружено.
Сейчас стоит воспользоваться пагинацией - постраничной загрузкой страниц.
Домен jsonplaceholder позволяет по определённым параметрам выводить лимитированное количество постов и менять страницу. В хедере (x-total-count) так же указывается максимальное количество постов, которое может выдать запрос
API / post.service.ts
utilities / pages.utilities.ts
Уже в основном компоненте страницы мы создаём три новых состояния, которые будут отвечать за общее количество страниц, лимит постов на странице и за саму страницу.
В хуке useFetching мы так же получаем заголовок от ответа и вызываем функцию getPageCount, которая делит заголовок на лимит и получает количество страниц, а уже дальше setTotalPages устанавливает это количество в качестве количества страниц.
Потом в pagesArray мы получаем массив страниц (1, 2, 3…).
И уже в функции changePage мы осуществляем фетчинг новых постов на странице. Эту функцию вызывает компонент Button.
Кнопки рендерятся в зависимости от количества элементов в массиве pagesArray.
page-components / Posts.tsx
Мы реализовали подгрузку новых постов на странице, но тут мы столкнулись с проблемой, что при переходе на разные страницы, у нас загружается прошлая выбранная страница
02:06:20 ➝ Обьяснение механизма изменения состояния
Тут нужно сказать, что хуки - это асинхронный процесс, который происходит внутри реакта не сразу. Функции по типу сеттеров useState копятся и выполняются разом, отчего и наше изменение страниц выше и не работает сразу. Это сделано для того, чтобы избежать повторных манипуляций с DOM-деревом
Самый простой способ решить данную проблему - это закинуть изменяющееся значение в useEffect и изменять страницу только через него.
И теперь можно из функции changePage убрать функцию фетчинга постов, так как это делает useEffect.
page-components / Posts.tsx
И так же можно декомпозировать постраничный вывод в отдельный компонент.
Компонент пагинации будет в себя принимать общее количество страниц, страницу, на которой мы сейчас находимся и функцию изменения страницы
components / Pagination / Pagination.props.ts
В сам компонент так же перенесём вызов функции получения массива страниц
components / Pagination / Pagination.tsx
Родительский компонент на данный момент выглядит подобным образом:
Создадим папку pages, которая будет хранить не отдельные компоненты, а целые страницы, которые будут располагаться в приложении
Компонент App, который является стартовой точкой приложения, будет хранить в себе BrowserRouter, который будет отслеживать все роуты в приложении
Чтобы определить страницу в качестве отдельного роута, нужно поместить компонент страницы в компонент отдельного роута Route. Сам Route внутрь себя принимает компонент для роутинга и путь, по которому страница должна отрисовываться
Теперь, чтобы реализовать полноценный роутинг и переход между страницами, нужно использовать компонент Link, который принимает путь to, который, в свою очередь, определяет, куда нужно перейти на странице.
Уже представленная реализация позволит нам переходить со страницы на страницу без перезагрузки страницы
Далее, чтобы мы могли перемещаться по роутам и обрабатывать несуществующие страницы, нужно добавить компонент Switch, в котором передать компонент Redirect, который уже будет перенаправлять нас на другую страницу, если та, на которую мы переходим, не будет существовать
И так выглядит страница ошибки при переходе не несуществующую страницу
pages > Error.jsx
Так же можно перенести всю логику роутинга в отдельный компонент
02:22:00 ➝ Динамическая навигация. useHistory, useParams. Загрузка комментариев к посту
И далее нам нужно реализовать роуты под каждый отдельный пост, чтобы можно было просмотреть по нему детальную информацию
Тут нужно уже воспользоваться хуком useHistory, который предоставляет реакт-роутер-дом. Этот хук позволяет нам реализовать переход по страницам без компонента Link. Конкретно мы можем воспользоваться методом push для генерации роутов по нашим постам. Переход будет осуществляться без помощи ссылок - мы нажали на кнопку и перешли на нужную страницу
components / PostItem.jsx
И сейчас наш роут заносится в поисковую строку, но ничего не происходит, так как роут поста не был создан
Создадим временную страницу отдельного поста
И далее в компоненте с роутами нужно реализовать переход на динамическую страницу. Чтобы указать, что параметр страницы динамический, нужно в ссылке указать :id двоеточие.
Так же у нас используется два роута, которые начинаются на /posts. Чтобы избежать конфликтов, нужно передать атрибут exact
Далее нам нужно подгружать информацию по посту (его имя, текст и так далее)
Первым делом, нужно добавить в API метод, который будет получать один определённый пост с сервера getById и его комментарии getCommentsByPostId, которые будут получать id поста
src > API > PostService.js
Сейчас временную страницу поста переделаем под полноценную страницу, которая будет получать информацию по посту с сервера
Далее идёт хук useParams, который тоже идёт из реакт-роутер-дом и позволяет получать пропсы определённого элемента по ссылке (например, при переходе на страницу поста данный хук возвращает id поста, так как в ссылке этот параметр динамический)
Ниже представлена реализация отдельной страницы с постом и его комментариями
02:33:10 ➝ Улучшаем навигацию. Приватные и публичные маршруты
Первым делом, чтобы сократить количество описанных роутов в ретёрне компонента роутинга, можно вынести определённые данные, которые принимает Route в отдельный массив объектов
Далее в компоненте с роутингом просто вывести все роуты через мапу
Сейчас нужно описать все приватные и публичные маршруты на странице
src > router > index.js
И чтобы выводить только публичные или только приватные маршруты, мы можем по тернарному оператору выводить разные конструкции с разными роутами
Если isAuth = false, то будет рендериться только один маршрут - Login
Далее у нас идёт хук useContext, который позволяет из любой точки приложения получить определённые данные.
Потребность в нём появилась, когда появились сложности с передачей пропсов между большой вложенностью компонентов в приложении
Чтобы начать использовать контекст, нам нужно для начала создать сам контекст, к которому мы буем обращаться:
src > context > index.js
Далее в главном компоненте приложения нужно обернуть всё последующее приложение в контекст AuthContext, который и будет распространять определённые данные на все компоненты. Самим распространением занимается Provider этого контекста. В атрибут value мы передаём значения, которые хотим распространять.
И теперь при обновлении страницы с отдельным постом мы можем встретить проблему, что мы выбрасываемся на страницу с постами. Это происходит по следующей причине:
изначально аутентификация стоит как false
у нас грузится страница логина
потом мы получаем данные с локального хранилища, что мы авторизованы
у нас загружается один из доступных роутов - посты
Чтобы поправить проблему, нужно создать состояние isLoading, которое не даст нам выбрасываться обратно, а просто будет отображать компонент загрузки
src > App.jsx
И далее в компоненте роутинга мы можем спокойно получить состояние входа и загрузки. Если мы находимся в загрузке, то можем просто вернуть лоадер, который покажет, что данные загружаются
Далее по состоянию аутентификации будут подгружаться определённые роуты
src > components > AppRouter.jsx
И так выглядит подгрузка данных по постам
На странице логина, мы будем устанавливать айтем auth в локальное хранилище, который и будет отвечать за аутентификацию пользователя в системе
src > page > Login.jsx
Далее в навигационном баре можно сделать функцию выхода из приложения, которая удалить айтем аутентификации из локального хранилища
src > components > Navbar.jsx
Итог: мы имеем страницу входа, с которой мы не можем пройти ни на какие приватные роуты / логин создаёт отметку в локальном хранилище о входе пользователя
Сейчас нужно написать хук, который позволит нам наблюдать, дошли ли мы до определённого блока на странице, чтобы спокойно загрузить следующую порцию контента.
Свойство isIntersecting у observer отвечает за то, в зоне видимости ли наш элемент
hooks / useObserver.ts
В return добавляем див-пустышку, который будет просто иметь в себе референс lastElement, за которым и будет следить обзёрвер. Далее нам просто нужно вызвать самописный хук useObserver и передать в него нужные параметры
page-components / Posts.tsx
При достижении невидимого div, у нас срабатывает функция закинутая в observer
Теперь, при достижении низа страницы, у нас автоматически подгружаются новые посты и перелистываются страницы