Это продолжение предыдущей статьи: Зачем писать свой React Data Grid в 2019
Для чего нужен Redux? Ответов много. Например, чтобы работать с общими данными в разных React-компонентах. Но можно воспринимать Redux еще как способ манипулирования компонентой. Сам взгляд интересный: любой React-компонент может управлять другим React-компонентом через Redux.
Возьмём React-компоненту, которая отображает данные в виде строк и колонок (Data Grid, грид). Каким функционалом у нее можно управлять? Составом колонок и строк. Выделением. Хорошо бы и прокруткой данных.
Например, некая React-компонента (Some Сomponent) могла бы управлять гридом так:
- отобрази такие-то строки и колонки;
- подсвети вхождение такого-то слова;
- выдели такую-то строку;
- выполни прокрутку к такой-то строке.
Управлять колонками не сложно. Достаточно положить в Redux настройки колонок: имена, порядок, ширины, маппинг на данные. Грид возьмет эти настройки и применит. С данными подход тот же.
Но давайте усложним задачу. Предположим, строк очень много. Их нельзя разом загрузить с сервера, и нельзя разом отобразить. Поэтому нужна порционная загрузка данных и порционное отображение данных.
Для порционного отображения возьмём виртуальный скроллинг, описанный в предыдущей статье. И попробуем скрестить его с порционной загрузкой и хранением в Redux. А также дадим возможность другим компонентам манипулировать загруженными данными и позицией скроллинга через Redux.
Это не абстрактная задача, а реальная задача из разрабатываемой нами ECM-системы:
Упорядочим требования. Что хотим получить?
- чтобы при скроллинге загружались новые порции данных;
- чтобы загруженные порции данных лежали в Redux;
- чтобы загруженными порциями можно было манипулировать из других компонент. Через Redux добавлять-удалять-изменять строки, и грид подхватывал эти изменения;
- чтобы позицией скроллинга можно было управлять из других компонент. Через Redux выполнить прокрутку к нужной строке.
Эти задачи мы и рассмотрим.
Небольшое отступление: виртуальный скроллинг из предыдущей статьи позволяет быстро проскроллить в любую часть грида. Например, в конец. Грид должен загрузить самую последнюю порцию данных, исключая все промежуточные, чтобы не тянуть с сервера тысячи строк. Поэтому порции не всегда загружаются последовательно, они могут быть загружены из разных частей списка.
Мы выбрали следующую схему по загрузке-хранению данных:
Грид в этой схеме делится на две части – компоненты Presentational и Container. Presentational занимается только отображением данных – это view. Данные показываются страницами (про это было рассказано в предыдущей статье). Container отвечает за загрузку данных и взаимодействие с Redux.
Пройдёмся по стрелкам схемы:
- Presentational не занимается загрузкой данных, он только сообщает через callback, каких данных ему не хватает для отображения. Presentational не знает про Redux, он не выполняет dispatch действий и не коннектится к хранилищу Redux.
- За загрузку данных отвечает Container. Эта компонента отправляет запрос на сервер при вызове callback. Container может запросить больше данных, чем требуется для отображения, чтобы минимизировать число запросов к серверу.
- Сервер присылает данные.
- Полученные данные Container отправляет в Redux. В Redux хранятся все загруженные порции данных, а не только последняя загруженная порция.
- Как только очередная порция данных попадёт в Redux, Container вытащит из Redux все порции.
- И отдаст их 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. Теперь перейдем к манипулированию. Самая частая задача — добавлять-удалять-изменять строки. Мы хотим, чтобы любая компонента веб-приложения могла это делать. Схема проста:
Some Component – это некоторая компонента веб-приложения, которая хочет управлять данными грида.
Пройдёмся по схеме:
- Все манипуляции с данными выполняются через редьюсеры Redux. Для добавления-удаления-изменения строки достаточно задиспатчить соответствующее действие (ADD_ROW, DELETE_ROW, UPDATE_ROW). Редьюсеры скорректируют данные в хранилище Redux.
- Как только данные изменяться в Redux, Grid Container вытащит актуальные данные из Redux.
- И отдаст их Presentational. Presentational актуализирует отрисованные страницы.
Управление скроллингом через Redux
Управлять скроллингом программно — это необходимый функционал. Самая распространенная ситуация — проскроллиться к выделенной записи. Например, пользователь создает новую запись в списке. Запись с учетом сортировки попадает в середину списка. Нужно программно выделить ее и проскроллиться к ней. И хорошо бы сделать это через 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, конечно же, не универсальны. Они подходят для разработанного нами грида, потому что мы оптимизировали грид под эти схемы, сделали у него соответствующее АПИ. Эти схемы не заработают для любого стороннего грида, поэтому воспринимайте статью, как один из примеров реализации взаимодействия с Redux.
Итоговое заключение (по 1 и 2 статье)
При разработке своего грида пришло понимание, что грид, в идеале, не часть нашего приложения, а независимый проект, который стоит выложить на github и развивать. Поэтому не стали использовать в коде предметные термины нашего приложения и добавлять лишние зависимости. Но с расширением функционала все сложнее этого придерживаться, ведь мы так и не выделили его в отдельный проект, а следовало сделать это сразу. Github все еще в планах.
Писать свой грид — это было правильное для нас решение. У нас было достаточно времени, чтобы реализовать все, что хотели (виртуализацию, работу с redux, порционную загрузку, работу с клавиатуры, работу с колонками, like-поиск с подсветкой и многое другое). Изначально мы сильно вложились в сторонний грид, в надежде, что он взлетит на наших ситуациях. Используя его, мы поняли, как вообще работают гриды, какие существуют проблемы, как их надо решать, и что мы в итоге хотим получить. И сделали свое решение.