ZarahioN Presents

Answering why

Реакт, Флукс, Состояние, весело [Pt. 6 – Why states]

In previous chapter…

Что же, не так давно, в прошлой статье мы познакомились с Store — Состоянием (я о модели Flux) в базовом исполнении, который нам предлагает сам реакт. И появляется закономерный вопрос — а что с ним можно делать?

И можно было бы ответить «реакт же библиотека для интерфейсов, чего еще надо (кроме форм)?», однако люди решили по-другому и технчески с состоянием можно делать что вздумается, в конце-концов это просто данные.

Например, мы можем при загрузке приложения включить интервал и получить, как ни странно, самый базовый игровой цикл (game loop):

class PseudoGame extends React.Component {
    state = { rotation: 0 }
    componentDidMount() {
        this.loop = window.setTimeout(this.loopFunction.bind(this), 16)
        // или requestAnimationFrame и подобные
    }
    loopFunction() {
        const { rotation } = this.state;
        this.setState({rotation: rotation + Math.random()})
        window.setTimeout(this.loopFunction.bind(this), 16);
    }
    render() {
        const { rotation } = this.state;
        return (
            <div style={{margin: '100px auto', width: 300, height: 300, border: '1px solid #ab00ff', transform: `rotate(${rotation}deg)`}} />)
        );
    }
}

Или реальная версия:

See the Pen React Rotating circle by Anton (@Askadar) on CodePen.

Такой незамысловатый код создаст неравномерно вращающийся квадрат и технически этот подход можно расширить до полноценной real-time игры с анимациями, движением и прочим. (Но без опыта лучше этого не делать и выбрать один из многочисленных доступных движков для создания игр на JS, при этом, я уверен, есть альтернативы в среде реакта)

Естественным образом вытекает уже данный ответ — мы вольны ваять из состояния все, что только пожелаем. Ведь как я уже не раз повторял — классическое состояние реакта это просто JS переменная, за которой реакт «следит» только за счет использования setState или вскользь упомянутого forceUpdate (, в практическом смысле, заглянуть под капот я пока не пытался).

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

За свою не очень долгую «карьеру» я успел, помимо банальных админок и форм, представить с помощью реакта шашки, в которые можно поиграть, zn парсер, текстовый markdown-подобный формат и даже черезмерно сложную и страшную систему проката, с таймерами, ценами и прочими математическими прелестями (которая оказалась немного неудобна из-за своей чрезмерной точности)

Однако, состояние в реакте хоть и является часто достаточным, но очень быстро становится неудобным и громоздким в некоторых критических случаях:
1. Вложенность\иерархия
2. Связи, особенно уровня братьев (siblings) или дети->родитель (children->parent)
Помимо того одним иногда неудобным недостатком состояния является императивный и нерасширяемый интерфейс setState.

Первая проблема — вложенность или сложная иерархия.

Под этой проблемой я подразумеваю большие деревья компонентов, которые очень часты в сложных приложениях. Сами по себе они достаточно безобидны, но что если, к примеру, мы создаем админку и нам нужно использовать имя пользователя в разных местах? Или нам просто нужно передать какие-то настройки всем (или большой части) компонентов?

Допустим мы храним его, то бишь имя пользователя, где-то достаточно высоко в иерархии компонентов, но тогда, чтобы показать его в сообщении приветствия одной из страниц или виджетов, нам придется спускать переменную с именем по всей цепочке от верхнего компонента до текста на странице, и в зависимости от размеров приложения и количества оберток путь может занимать пяток-другой промежуточных компонентов, в каждом из которых нужно передать это свойство дальше по цепочке. Хорошая перспектива, не находишь?

Для этой проблемы было два способа решения, просто передача всех не взятых свойств через ...rest и контекст, который не особо рекомендуют использовать сами разработчики реакта.
Рекомендация вполне простая — это апи не предназначено для обычной публики из пользователей библиотек — то есть по сути нас, оно предназначено для создателей библиотек. Собственно создатели и привнесли третий (или второй с половиной) способ решения проблемы, предоставив в начале Redux а позже и MobX с еще парочкой альтернатив — полноценных представителей состояния (Store) и в некоторой мере связки Диспетчера и Действий. Но о них чуть позже, перед этим:

Вторая проблема — связи между компонентами.

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

Представь такую воодушевляющую и прекрасную картину: мы все еще корпеем над нашей админкой, где кто-то зачем-то придумал писать персональное приветствие на каждом виджете. И, сюрприз, этот кто-то захотел помимо персонального приветствия показать аватарку человека, да при том при клике на аватарку открыть модальное окно профиля, которое у нас висит неподалеку от корня нашего приложения. Ну и естественно оно находится не в нашем дереве Content, а в заголовке страницы — компоненте Header.

- Root
- \ Header
- - \ Profile
- \ Content
- - \ Menu
- - \ Page
- - - \ Title
- - - \ HelloUser
- - - \ Article

Задача простая — переключить видимость модалки профиля, которая хранится у него в состоянии. Вспоминая наставления и не имея доступа к гуглу благодаря креативному руководству, мы решаемся на единственное известное решение — как-то передать доступ к состоянию профиля нашей странице (надеюсь ты уже запутался и тихо вопрошаешь «за что?..»).

Но.. мы не можем просто мимоходом сбросить функцию которая изменит состояние профиля в страницу — единственная связь между ними это корень приложения, где они разделяются на заголовок и содержание страницы. А в отличии от имени пользователя хранящегося в корне, видимость профиля находится непосредственно в нем самом. Однако, выход есть, креативный ум работника такого заведения подсказывает возможность — передаем в профиль функцию, которая вернет в корень функцию через которую можно изменить состояние профиля и которую мы уже можем сбросить в страницу. Ура, мы избежали кризиса и нас не уволят. К сожалению.

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

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

Как же редакс решил проблему связей? Все тот же контекст — апи, хоть и вшитое в реакт, однако слишком грубое и изменчивое, чтобы использовать его напрямую, позволяет «достаточно простым» образом распределить данные по всему приложению через доступ к «глобальным» объектам. Кавычки не зря — апи контекста не самое понятное и прямолинейное, поэтому лезть в его дебри не имя стоящих причин не самая лучшая затея, и я этим не занимался — как следствие надежно и подробно распространяться о нем я не могу.

Redux, как и MobX мы будем подробнее разбирать в будущем, но я вкратце опишу как с первым решаются обе проблемы:

Проблема первая, вложенность или передача каких-то параметров вглубь дерева компонентов. Редакс предоставляет единое дерево состояния, части которого можно брать в любой компонент независимо от его положения в дереве компонентов (сколько деревьев). Таким образом можно создать объект User в дереве редакса и подключить его к компонентам Profile, HelloUser и любым другим, где нужны данные о пользователе.

Проблема вторая, связи между компонентами и тем более изменение данных других компонентов. Помимо предоставления данных редакс может поделиться своей реализацией создателя Действий, которые ловит редакс (Диспетчер) и передает в созданные нами обработчики, которые обновляют Состояние (ничего не начинает напоминать?). Таким образом проблема открытия модалки с профилем решается за счет выноса флажка «открыто ли» окна в дерево редакса и отправка действия на открытие этого окна идентичного из кнопки заголовка и страницы.

Теперь, лично в моем мнении, реакт начинает напоминать безупречный балет: пользователь нажимает на кнопку, это создает действие, которое получает наш обработчик, обновляющий состояние приложения, вследствие которого открывается окно профиля. Все плавно и последовательно перетекает из старого состояния в новое.
Впрочем, ты вполне можешь оказаться не столь легко воодушевляем и все еще относиться скептически ко всей кокофонии вокруг реакта и редакса и.. я ничем не могу тебе помочь. Из моих скромных двух с бонусом лет работы кодящей мартышкой реакт это наиболее удобный способ построения внешнего вида приложения. Я не стану утверждать о его скорости, эффективности или простоте изучения, но по удобству разработки я не знаю ничего, что может приблизиться к этому уровню (но я верю в свой скромный опыт). Единственное, что я могу еще предложить для твоего убеждения это мое спокойствие ума и знание что я (и многие другие разработчики) могу сотворить все, что пожелается, без слишком больших и неприятных болей в нижней части спины.

Впрочем, возвращаемся на землю, несмотря на многочисленные плюсы, у редакса есть интересный неоднозначный минус — он очень многословный и «странный». Вкратце, чтобы подключить редакс «правильно» нужно добавить конфигурацию (как правило в index.js), нужно добавить файлы обработчика, создателя действий и типов действий. На каждый компонент или обработчик. Получается где-то 3 файла на каждый контейнер (это термин выданный редаксом для компонентов с логикой и состоянием). Очень быстро ориентироваться во всем этом великолепии становится мягко скажем страшно.

Однако, как часто бывает — правильно не совсем хорошо, многословность редакса можно (и нужно) обращать в пользу — она предоставляет удивительный простор для семантического описания творящегося в приложение — создатель действий showProfile создает действие profileShowed забирая которое обработчик «понимает» что нужно изменить флажок в состоянии и получив новое состояние реакт отрисовывает стиль модалки в нужном состоянии по обновленному флажку.
И предоставляя простор для семантики он дает возможность ее и уничтожить запутывая других разработчиков приложения странными и страшными названиями — любые стилистические выборы кода должны быть понятны и согласованы между всей командой. Я не раз видел запутавшихся или не желавших разбираться в редаксе разработчиков только из-за его странной и часто неприятно использованной многословности, с таким мощным инструментов нужно обращаться очень внимательно и осторожно.

На этой серьезной ноте я завершу наш краткий экскурс в теорию Состояния, так и не ответив на последний скромный вопрос о недостатках встроенного в реакт версии. О них и отличиях между всеми вариациями правления чуть позже.

Leave a Reply

Ваш e-mail не будет опубликован. Обязательные поля помечены *