Главная » Хабрахабр » Как в React избавиться от сложности в управлении состоянием — отчёт по итогам поездки на React Amsterdam

Как в React избавиться от сложности в управлении состоянием — отчёт по итогам поездки на React Amsterdam

В апреле посчастливилось побывать на очень крутом мероприятии — React Amsterdam. Кроме приятных организационных моментов было ещё и много интересных докладов. Они были, в основном, прикладного характера. Поскольку стек технологий в принципе устоялся, докладчики рассказывали о способах решения практических проблем, а не продвигали что-то незнакомое и революционное. Здесь я расскажу подробнее о выступлении “setState Machine” Микеле Бертоли из Facebook.

Существует много полезных пакетов, которые решают эту задачу, однако же нередко приходится писать самостоятельно. Основная проблема, которой был посвящен доклад, — сложность в управлении состоянием в React.
Для примера давайте реализуем всем знакомую функциональность — Infinite Scroll, то есть подгрузку данных при прокрутке пользователем до конца (или почти до конца) страницы.

Что нам для этого потребуется сделать в нашем компоненте:

  1. Добавить обработчик для события scroll.
  2. Добавить проверку, пролистал ли пользователь до нужного места, с которого будем подгружать данные.
  3. Собственно, подгрузить данные.

В первом приближении этого достаточно, однако давайте добавим еще несколько требований для корректной работы:

  1. Не подгружать новые данные, если предыдущая порция еще грузится.
  2. Как-то информировать пользователя о том, что данные грузятся — показать loading или что-то в этом роде на время загрузки.
  3. Не начинать подгрузку данных, если уже всё загружено.
  4. Добавить отображение ошибки пользователю.

Давайте представим, что должно храниться в нашем состоянии, помимо данных, чтобы наша функциональность работала правильно:

  1. Флаг isFetching — показывает нам, что сейчас грузятся данные.
  2. Поле error — должно содержать информацию об ошибке.
  3. Поле isEmpty — показывает нам, что данных нет.
  4. Если вдруг мы захотим добавить функциональность для retry, то нужно хранить информацию и для неё.

Какие основные недостатки у такой реализации:

  1. Большая привязка к контексту. Очень много условий, например, грузим данные только тогда, когда прокрутили до нужного места, при этом не грузятся предыдущие данные, и т.д.
  2. Наш код сложно читать и понимать.
  3. Сложно масштабировать — при добавлении нового свойства в состояние нужно пройтись по всем нашим условиям и понять, как нужно менять состояние в том или ином месте, чтоб не сломать логику. Это также может привести к багам.

Исправить все эти недостатки нам поможет машина состояний (State Machine).

По сути, это принцип использования конечного автомата состояний (Final State Machine), некой абстрактной модели, содержащей конечное число состояний.

Описывается модель при помощи пяти параметров:

  1. Все состояния, в которых может находится автомат.
  2. Набор всех входных данных, принимаемых автоматом.
  3. Функция переходов — принимает предыдущее состояние и набор входных данных, возвращает новое состояние.
  4. Начальное состояние.
  5. Конечное состояние.

В каждый момент времени активным может быть лишь одно состояние.

Это машина с тремя состояниями, мы знаем их очередность, а также условно можем назвать начальное и конечное состояние. Соответственно, мы можем определить условия перехода из одного состояния в другое.
В качестве примера рассмотрим работу светофора.

Это обертка над еще одной библиотекой xstate — функциональные JS-машины состояний без сохранения состояния и диаграммы состояний. Давайте добавим в наш код библиотеку react-automata — абстракцию машины состояний для React.

Чтобы понять, как эта теория применима к нашему случаю, давайте посмотрим, как функциональность будет выглядеть в виде statechart:

Когда мы готовы к дальнейшей работе, посылаем машине событие READY, и машина переходит в состояние загрузки. Для начала укажем начальное состояние — точку входа, которой является добавление события прокрутки. Когда при прокрутке выполняется условие подгрузки новой порции данных, мы переходим в состояние загрузки и можем пребывать в этом цикле, пока не загрузим все данные. Если загрузка была успешной и пока не все данные загружены, мы переходим в состояние прослушивания. Тогда мы больше не слушаем событие.

Схематично наш код может выглядеть так:

import React from 'react'
import from 'react-hot-loader'
import { Action, withStatechart } from 'react-automata' export const statechart = { // начальное состояние initial: 'attach', // Список состояний states: { attach: { on: { READY: 'fetching', }, }, fetching: { on: { SUCCESS: { listening: { //Переходим в состояние listening по событию SUCCESS //cond - pure функция, переводящая машину в указанное состояние, если возвращает правдивое значение cond: extState => extState.hasMore, }, detach: { cond: extState => !extState.hasMore, }, }, ERROR: 'listening', }, // fetch - событие, которое должно быть выполнено при входе в состояние fetching onEntry: 'fetch', }, listening: { on: { SCROLL: 'fetching', }, }, detach: {}, },
} class InfiniteScroll extends React.Component { componentDidMount() { // на mount нашего компонента переходим из начального состояния в fetching this.attach() } attach() { //навешиваем наш обработчик и переходим в состояние fetching //возможна, конечно, и другая реализация этого перехода - зависит от требований к работе фичи this.element.addEventListener('scroll', this.handleScroll) this.props.transition('READY')
} handleScroll = e => { const { scrollTop, scrollHeight, clientHeight } = e.target const isCilentAtBottom = 0.9 * (scrollHeight - scrollTop) === clientHeight if (isCilentAtBottom) { // Переход из listening в fetching this.props.transition('SCROLL') }
} fetch() { const { transition } = this.props loadTodos() .then(res => res.json()) .then(data => transition('SUCCESS', { todos: data })) .catch(() => transition('ERROR'))
} render() { // Action - компонент, который определяет, что должно рендериться для данного события return ( <div ref={element => { this.element = element }} > <Action show="fetch">Loading...</Action> <ul> {this.props.todos.map(todo => <li key={todo.id}>{todo.text}</li>)} </ul> </div> ) }
} InfiniteScroll.defaultProps = { todos: [],
} const initialData = { todos: [], devTools: true } const StateMachine = withStatechart(statechart, initialData)(InfiniteScroll) export default hot(module)(StateMachine)

Мы используем hoc withStatechart из react-automata, передаем наши начальные данные, и теперь в props доступны метод transition для изменения состояния машины, и machineState — текущее состояние машины.

Переменная statechart — это программное описание нашего рисунка.

Преимущества подхода:

  1. Меньше багов.
  2. Проще читать и понимать код.
  3. Разделение того, что случилось и когда случилось. Первое управляется компонентом, второе — диаграммами состояний.

Полезные ссылки:

  1. Доклад Микеле Бертоли на React Amsterdam 2018: https://www.youtube.com/watch?v=smBND2pwdUE&t=3137s
  2. React Automata: https://github.com/MicheleBertoli/react-automata
  3. Документация по xstate: http://davidkpiano.github.io/xstate/docs/#/
  4. Объяснение диаграмм состояний: http://www.inf.ed.ac.uk/teaching/courses/seoc/2005_2006/resources/statecharts.pdf

Оставить комментарий

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

*

x

Ещё Hi-Tech Интересное!

Третья проверка Qt 5 с помощью PVS-Studio

Время от времени наша команда повторно проверяет проекты, про которые мы уже писали статьи. Очередным таким перепроверенным проектом стал Qt. Последний раз мы проверяли его с помощью PVS-Studio в 2014 году. Начиная с 2014 года проект начал регулярно проверяться с ...

[Из песочницы] Разборка движка визуальных новелл Qlie

Пожалуй, подавляющее большинство всех визуальных новелл было выпущено на японском языке, лишь немногие были переведены на английский(официально или любителями) и еще меньше было переведено на другие языки. Любительский перевод визуальных новелл, если сравнивать с переводами других игр, имеет ряд особенностей ...