86.62
Rating
Directum
Цифровизация процессов и документов
30 August 2019

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

Directum corporate blogWebsite developmentJavaScriptReactJS

Это продолжение предыдущей статьи: Зачем писать свой 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-поиск с подсветкой и многое другое). Изначально мы сильно вложились в сторонний грид, в надежде, что он взлетит на наших ситуациях. Используя его, мы поняли, как вообще работают гриды, какие существуют проблемы, как их надо решать, и что мы в итоге хотим получить. И сделали свое решение.

Tags:javascriptreactreduxgrid
Hubs: Directum corporate blog Website development JavaScript ReactJS
+10
5k 45
Comments 3
Information
Founded

15 September 1988

Location

Россия

Employees

501–1,000 employees

Registered

1 July 2019