Как управлять React Data Grid через Redux

от автора

Это продолжение предыдущей статьи: Зачем писать свой React Data Grid в 2019

Для чего нужен Redux? Ответов много. Например, чтобы работать с общими данными в разных React-компонентах. Но можно воспринимать Redux еще как способ манипулирования компонентой. Сам взгляд интересный: любой React-компонент может управлять другим React-компонентом через Redux.

Возьмём React-компоненту, которая отображает данные в виде строк и колонок (Data Grid, грид). Каким функционалом у нее можно управлять? Составом колонок и строк. Выделением. Хорошо бы и прокруткой данных.

image

Например, некая React-компонента (Some Сomponent) могла бы управлять гридом так:

  • отобрази такие-то строки и колонки;
  • подсвети вхождение такого-то слова;
  • выдели такую-то строку;
  • выполни прокрутку к такой-то строке.

Управлять колонками не сложно. Достаточно положить в Redux настройки колонок: имена, порядок, ширины, маппинг на данные. Грид возьмет эти настройки и применит. С данными подход тот же.

Но давайте усложним задачу. Предположим, строк очень много. Их нельзя разом загрузить с сервера, и нельзя разом отобразить. Поэтому нужна порционная загрузка данных и порционное отображение данных.

Для порционного отображения возьмём виртуальный скроллинг, описанный в предыдущей статье. И попробуем скрестить его с порционной загрузкой и хранением в Redux. А также дадим возможность другим компонентам манипулировать загруженными данными и позицией скроллинга через Redux.

Это не абстрактная задача, а реальная задача из разрабатываемой нами ECM-системы:
image

Упорядочим требования. Что хотим получить?

  • чтобы при скроллинге загружались новые порции данных;
  • чтобы загруженные порции данных лежали в Redux;
  • чтобы загруженными порциями можно было манипулировать из других компонент. Через Redux добавлять-удалять-изменять строки, и грид подхватывал эти изменения;
  • чтобы позицией скроллинга можно было управлять из других компонент. Через Redux выполнить прокрутку к нужной строке.

Эти задачи мы и рассмотрим.

Небольшое отступление: виртуальный скроллинг из предыдущей статьи позволяет быстро проскроллить в любую часть грида. Например, в конец. Грид должен загрузить самую последнюю порцию данных, исключая все промежуточные, чтобы не тянуть с сервера тысячи строк. Поэтому порции не всегда загружаются последовательно, они могут быть загружены из разных частей списка.

Мы выбрали следующую схему по загрузке-хранению данных:
image

Грид в этой схеме делится на две части – компоненты Presentational и Container. Presentational занимается только отображением данных – это view. Данные показываются страницами (про это было рассказано в предыдущей статье). Container отвечает за загрузку данных и взаимодействие с Redux.

Пройдёмся по стрелкам схемы:

  1. Presentational не занимается загрузкой данных, он только сообщает через callback, каких данных ему не хватает для отображения. Presentational не знает про Redux, он не выполняет dispatch действий и не коннектится к хранилищу Redux.
  2. За загрузку данных отвечает Container. Эта компонента отправляет запрос на сервер при вызове callback. Container может запросить больше данных, чем требуется для отображения, чтобы минимизировать число запросов к серверу.
  3. Сервер присылает данные.
  4. Полученные данные Container отправляет в Redux. В Redux хранятся все загруженные порции данных, а не только последняя загруженная порция.
  5. Как только очередная порция данных попадёт в Redux, Container вытащит из Redux все порции.
  6. И отдаст их Presentational. Presentational не обязан отрисовать все полученные данные, он отображает только то, что попадает во viewport. При этом загруженные данные и отрисованные страницы – это не одно и тоже. Может быть загружено 1000 записей одним блоком, а отображено 50 записей двумя страницами.

Приведу псевдокод этой схемы:

class GridContainer extends React.Component<Props> {   props: Props;    render(): React.Element<any> {     return <Grid       // Отдаем гриду все загруженные данные.       dataSource={this.props.data}       // Callback на загрузку недостающих данных.       loadData={this.props.loadData} />;   } }

const mapStateToProps = (state) => {   return { data: state.data }; };  const mapDispatchToProps = (dispatch) => {   return {     loadData: async (skip: number, take: number) => {       // Загружаем данные с сервера.       const page: Page = await load(skip, take);       // Добавляем загруженные данные в Redux.       dispatch({ type: ADD_PAGE, page });     }   }; };  export default connect(mapStateToProps, mapDispatchToProps)(GridContainer);

Используемые типы в псевдокоде:

type Props = {   data: DataSource,   loadData: (skip: number, take: number) => void };  type DataSource = {   // Загруженные порции данных.   pages: Array<Page>,   // Количество строк в гриде.   totalRowsCount: number };  type Page = {   // Индекс, с которого загружены строки.   startIndex: number,   // Строки.   rows: Array<Object> };

С первой задачей справились — порционно загружать и хранить данные в Redux. Теперь перейдем к манипулированию. Самая частая задача — добавлять-удалять-изменять строки. Мы хотим, чтобы любая компонента веб-приложения могла это делать. Схема проста:

image

Some Component – это некоторая компонента веб-приложения, которая хочет управлять данными грида.

Пройдёмся по схеме:

  1. Все манипуляции с данными выполняются через редьюсеры Redux. Для добавления-удаления-изменения строки достаточно задиспатчить соответствующее действие (ADD_ROW, DELETE_ROW, UPDATE_ROW). Редьюсеры скорректируют данные в хранилище Redux.
  2. Как только данные изменяться в Redux, Grid Container вытащит актуальные данные из Redux.
  3. И отдаст их Presentational. Presentational актуализирует отрисованные страницы.

Управление скроллингом через Redux

Управлять скроллингом программно — это необходимый функционал. Самая распространенная ситуация — проскроллиться к выделенной записи. Например, пользователь создает новую запись в списке. Запись с учетом сортировки попадает в середину списка. Нужно программно выделить ее и проскроллиться к ней. И хорошо бы сделать это через Redux.

image

Управлять выделением через Redux не сложно, но как управлять скроллингом?
Для этого в Redux Store мы положим два поля:

  // Индекс строки, к которой нужно проскроллиться.   scrollToIndex: ?number,   // Сигнал, что нужно выполнить скроллинг.   scrollSignal: number

Поле scrollToIndex понятное. Хочешь выполнить скроллинг, тогда установи в scrollToIndex номер нужной строки. Этот номер будет передан гриду, и грид тут же выполнит скроллинг к ней:

image

Для чего поле scrollSignal? Оно решает проблему повторного скроллинга к тому же индексу. Если мы уже выполнили программный скроллинг к индексу 100, то повторно выполнить скроллинг к этому же индексу не получится. Поэтому используется поле scrollSignal, при изменении которого грид повторно выполнит скроллинг к scrollToIndex. ScrollSignal инкрементируется автоматически в редьюсере при обработке действия SCROLL:

image

Псевдокод управления скроллингом:

class GridContainer extends React.Component<Props> {   props: Props;    render(): React.Element<any> {     return <Grid       // Отдаем гриду все загруженные данные.       dataSource={this.props.data}       // Индекс строки, к которой нужно проскроллиться..       scrollToIndex={this.props.scrollToIndex}       // Сигнал, что нужно выполнить скроллинг.       scrollSignal={this.props.scrollSignal} />;   } }

const mapStateToProps = (state) => {   return {     data: state.data,     scrollToIndex: state.scrollToIndex,     scrollSignal: state.scrollSignal    }; };  export default connect(mapStateToProps)(GridContainer);

Используемые типы в псевдокоде:

type Props = {   data: DataSource,   scrollToIndex: ?number,   scrollSignal: number };

Заключение (по Redux)

Предложенные схемы взаимодействия с Redux, конечно же, не универсальны. Они подходят для разработанного нами грида, потому что мы оптимизировали грид под эти схемы, сделали у него соответствующее АПИ. Эти схемы не взлетят для любого стороннего грида, поэтому воспринимайте статью, как один из примеров реализации взаимодействия с Redux.

Итоговое заключению (по 1 и 2 статье)

При разработке своего грида пришло понимание, что грид, в идеале, не часть нашего приложения, а независимый проект, который стоит выложить на github и развивать. Поэтому не стали использовать в коде предметные термины нашего приложения и добавлять лишние зависимости. Но с расширением функционала все сложнее этого придерживаться, ведь мы так и не выделили его в отдельный проект, а следовало сделать это сразу. Github все еще в планах.

Писать свой грид — это было правильное для нас решение. У нас было достаточно времени, чтобы реализовать все, что хотели (виртуализацию, работу с redux, порционную загрузку, работу с клавиатуры, работу с колонками, like-поиск с подсветкой и многое другое). Изначально мы сильно вложились в сторонний грид, в надежде, что он взлетит на наших ситуациях. Используя его, мы поняли, как вообще работают гриды, какие существуют проблемы, как их надо решать, и что мы в итоге хотим получить. И сделали свое решение.


ссылка на оригинал статьи https://habr.com/ru/company/directum/blog/465585/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *