- PVSM.RU - https://www.pvsm.ru -
В апреле посчастливилось побывать на очень крутом мероприятии — React Amsterdam [1]. Кроме приятных организационных моментов было ещё и много интересных докладов. Они были, в основном, прикладного характера. Поскольку стек технологий в принципе устоялся, докладчики рассказывали о способах решения практических проблем, а не продвигали что-то незнакомое и революционное. Здесь я расскажу подробнее о выступлении “setState Machine” Микеле Бертоли [2] из Facebook.
Основная проблема, которой был посвящен доклад, — сложность в управлении состоянием в React.
Для примера давайте реализуем всем знакомую функциональность — Infinite Scroll, то есть подгрузку данных при прокрутке пользователем до конца (или почти до конца) страницы. Существует много полезных пакетов, которые решают эту задачу, однако же нередко приходится писать самостоятельно.
Что нам для этого потребуется сделать в нашем компоненте:
scroll
.В первом приближении этого достаточно, однако давайте добавим еще несколько требований для корректной работы:
Давайте представим, что должно храниться в нашем состоянии, помимо данных, чтобы наша функциональность работала правильно:
isFetching
— показывает нам, что сейчас грузятся данные.error
— должно содержать информацию об ошибке.isEmpty
— показывает нам, что данных нет.retry
, то нужно хранить информацию и для неё.Какие основные недостатки у такой реализации:
Исправить все эти недостатки нам поможет машина состояний (State Machine).
По сути, это принцип использования конечного автомата состояний (Final State Machine), некой абстрактной модели, содержащей конечное число состояний.
Описывается модель при помощи пяти параметров:
В каждый момент времени активным может быть лишь одно состояние.
Соответственно, мы можем определить условия перехода из одного состояния в другое.
В качестве примера рассмотрим работу светофора. Это машина с тремя состояниями, мы знаем их очередность, а также условно можем назвать начальное и конечное состояние.
Давайте добавим в наш код библиотеку react-automata [3] — абстракцию машины состояний для React. Это обертка над еще одной библиотекой xstate [4] — функциональные JS-машины состояний без сохранения состояния и диаграммы состояний.
Чтобы понять, как эта теория применима к нашему случаю, давайте посмотрим, как функциональность будет выглядеть в виде statechart:
Для начала укажем начальное состояние — точку входа, которой является добавление события прокрутки. Когда мы готовы к дальнейшей работе, посылаем машине событие READY, и машина переходит в состояние загрузки. Если загрузка была успешной и пока не все данные загружены, мы переходим в состояние прослушивания. Когда при прокрутке выполняется условие подгрузки новой порции данных, мы переходим в состояние загрузки и можем пребывать в этом цикле, пока не загрузим все данные. Тогда мы больше не слушаем событие.
Схематично наш код может выглядеть так:
import React from 'react'
import { hot } 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
— это программное описание нашего рисунка.
Преимущества подхода:
Полезные ссылки:
Автор: NIX_Solutions
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/282618
Ссылки в тексте:
[1] React Amsterdam: https://react.amsterdam/
[2] Микеле Бертоли: https://github.com/MicheleBertoli
[3] react-automata: https://github.com/MicheleBertoli/react-automata
[4] xstate: https://github.com/davidkpiano/xstate
[5] https://www.youtube.com/watch?v=smBND2pwdUE&t=3137s: https://www.youtube.com/watch?v=smBND2pwdUE&t=3137s
[6] http://davidkpiano.github.io/xstate/docs/#/: http://davidkpiano.github.io/xstate/docs/#/
[7] http://www.inf.ed.ac.uk/teaching/courses/seoc/2005_2006/resources/statecharts.pdf: http://www.inf.ed.ac.uk/teaching/courses/seoc/2005_2006/resources/statecharts.pdf
[8] Источник: https://habr.com/post/354106/?utm_source=habrahabr&utm_medium=rss&utm_campaign=354106
Нажмите здесь для печати.