Как управлять React Data Grid через Redux
- суббота, 31 августа 2019 г. в 00:30:11
Это продолжение предыдущей статьи: Зачем писать свой React Data Grid в 2019
Для чего нужен Redux? Ответов много. Например, чтобы работать с общими данными в разных React-компонентах. Но можно воспринимать Redux еще как способ манипулирования компонентой. Сам взгляд интересный: любой React-компонент может управлять другим React-компонентом через Redux.
Возьмём React-компоненту, которая отображает данные в виде строк и колонок (Data Grid, грид). Каким функционалом у нее можно управлять? Составом колонок и строк. Выделением. Хорошо бы и прокруткой данных.

Например, некая React-компонента (Some Сomponent) могла бы управлять гридом так:
Управлять колонками не сложно. Достаточно положить в Redux настройки колонок: имена, порядок, ширины, маппинг на данные. Грид возьмет эти настройки и применит. С данными подход тот же.
Но давайте усложним задачу. Предположим, строк очень много. Их нельзя разом загрузить с сервера, и нельзя разом отобразить. Поэтому нужна порционная загрузка данных и порционное отображение данных.
Для порционного отображения возьмём виртуальный скроллинг, описанный в предыдущей статье. И попробуем скрестить его с порционной загрузкой и хранением в Redux. А также дадим возможность другим компонентам манипулировать загруженными данными и позицией скроллинга через Redux.
Это не абстрактная задача, а реальная задача из разрабатываемой нами ECM-системы:

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

Грид в этой схеме делится на две части – компоненты Presentational и Container. Presentational занимается только отображением данных – это view. Данные показываются страницами (про это было рассказано в предыдущей статье). Container отвечает за загрузку данных и взаимодействие с Redux.
Пройдёмся по стрелкам схемы:
Приведу псевдокод этой схемы:
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. Теперь перейдем к манипулированию. Самая частая задача — добавлять-удалять-изменять строки. Мы хотим, чтобы любая компонента веб-приложения могла это делать. Схема проста:

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

Управлять выделением через Redux не сложно, но как управлять скроллингом?
Для этого в Redux Store мы положим два поля:
// Индекс строки, к которой нужно проскроллиться.
scrollToIndex: ?number,
// Сигнал, что нужно выполнить скроллинг.
scrollSignal: numberПоле scrollToIndex понятное. Хочешь выполнить скроллинг, тогда установи в scrollToIndex номер нужной строки. Этот номер будет передан гриду, и грид тут же выполнит скроллинг к ней:

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

Псевдокод управления скроллингом:
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.
При разработке своего грида пришло понимание, что грид, в идеале, не часть нашего приложения, а независимый проект, который стоит выложить на github и развивать. Поэтому не стали использовать в коде предметные термины нашего приложения и добавлять лишние зависимости. Но с расширением функционала все сложнее этого придерживаться, ведь мы так и не выделили его в отдельный проект, а следовало сделать это сразу. Github все еще в планах.
Писать свой грид — это было правильное для нас решение. У нас было достаточно времени, чтобы реализовать все, что хотели (виртуализацию, работу с redux, порционную загрузку, работу с клавиатуры, работу с колонками, like-поиск с подсветкой и многое другое). Изначально мы сильно вложились в сторонний грид, в надежде, что он взлетит на наших ситуациях. Используя его, мы поняли, как вообще работают гриды, какие существуют проблемы, как их надо решать, и что мы в итоге хотим получить. И сделали свое решение.