Если вы еще не слышали о реактивном программировании, приглашаю вас сделать паузу и познакомиться. This gist - отличное место для начала. Реактивное программирование позволяет вашим приложениям реагировать на события и потоки данных в реальном времени. Это означает, что вы можете пропустить такие методы жизненного цикла, как componentDidMount и componentDidUpdate, при проверке изменений в состоянии вашего приложения. Если вы уже знакомы с реактивным программированием, то, надеюсь, этот пост может дать вам больше информации о том, как можно легко согласовать его с React с помощью библиотеки перекомпоновки.

Если вы хотите пропустить до конца, кодпендинг находится здесь



Обновление: если вам интересно, как это может выглядеть с хуками React, я написал об этом в блоге здесь: https://teukka.tech/datastructures.html

Задача

Изучая реактивное программирование в контексте React, я нашел много отличного материала, но не так много руководств, кроме простого извлечения и подсчета данных. Здесь мы построим набор из трех списков. Первый список содержит пользователей, а два других контейнера перечисляют их симпатии и антипатии.

Щелчок по пользователю должен затем загрузить его список лайков и антипатий и изменить URL-адрес, чтобы указать, какой пользователь в данный момент выбран. Если в URL-адресе не указан пользователь, то автоматически должен быть выбран первый пользователь в списке. Отметки «Нравится» и «Не нравится» также должны иметь возможность добавлять, удалять и редактировать.

Инструменты

Основными инструментами, которые мы будем использовать, являются перекомпоновка и RXJS. Оба являются служебными библиотеками, которые помогут вам использовать возможности реактивного программирования. Я бы посоветовал просмотреть документы обеих библиотек, чтобы познакомиться с их использованием, но я также объясню, как их использовать в контексте создания пользовательского интерфейса в React.

Почему

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

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

Первые шаги

Первое, что нам нужно сделать в этом проекте, - это настроить перекомпоновку для работы с наблюдаемой конфигурацией, используемой RXJS. Это делается путем импорта функции setObservableConfig из перекомпоновки и передачи ей утилиты from из rxjs.

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

Давайте разберем это построчно.

Во-первых, мы импортируем несколько операторов из rxjs, поэтому давайте обсудим, что они делают и зачем они нам нужны. switchMap и map позволяют нам сопоставлять значения с другими функциями и компонентами. switchMap берет наблюдаемое и сглаживает его, позволяя нам передавать только его значения следующей функции, а не саму наблюдаемую.

Следующие ниже операторы tap, catchError и startWith помогают нам в отладке и позволяют нам предоставлять пользователю информацию о состоянии приложения. tap принимает и наблюдаемое, выполняет побочный эффект и возвращает копию исходного наблюдаемого.

Использование tap для отладки наблюдаемых

Используя tap, мы можем выйти из ответа, полученного от бэкэнда, не беспокоясь об изменении чего-либо в нашем потоке. Использование операторов startWith и catchError помогает нам сообщать пользователям о состоянии приложения, давая нам возможность выполнять условный рендеринг на основе его текущего состояния. Мы займемся реализацией этого позже.

Теперь, когда мы понимаем, на что способны используемые операторы, давайте рассмотрим логику потока загрузки. Вы заметите, что переменная load сама по себе является результатом функции mapPropsStream, взятой из recompose, она принимает в качестве аргумента функцию, которая получает поток реквизитов (потоки обозначены суффиксом $ для ясности, но это не имеет синтаксического значения. ), который затем можно передать через наши логические операторы.

Вот где switchMap становится очень полезным. Поскольку React не может ничего отобразить с помощью объекта Observable, нам нужно сгладить наш поток props, чтобы мы получали только значения этого потока. Эти значения - фактические реквизиты, которые нам нужны для рендеринга нашего компонента. Итак, мы сглаживаем поток и сопоставляем его с нашей функцией, которая выбирает пользователей из бэкэнда. Вы можете использовать props здесь, чтобы передать любые дополнительные аргументы вашей функции, которая получает данные, но, поскольку мы здесь имитируем, нам не нужно ничего передавать. Наконец, наш запрос возвращает массив users, который мы затем сопоставляем с объектом, который передает props, users, и сигнализирует об успешном запросе, устанавливая status на 'SUCCESS'.

Возможно, вы заметили, что здесь мы также используем tap для вызова функции setUserList, которая содержится в реквизитах, передаваемых в поток загрузки. Это в сочетании с тройной проверкой isEmpty(props.userList поможет нам предотвратить ненужную повторную выборку данных. Если у нас есть данные, мы просто хотим вернуть их без каких-либо манипуляций.

Использование потоков для обработки событий DOM

Давайте создадим второй поток props, который обрабатывает выбор пользователя из списка и устанавливает его в качестве выбранного пользователя:

Как и в нашем потоке load, мы хотим создать поток свойств, используя функцию mapPropsStream рекомпозиции. Однако вместо того, чтобы напрямую назначать свойства switchMapping, мы хотим создать две переменные - поток и обработчик - используя другую служебную функцию из Recompose, createEventHandler. createEventHandler, как следует из названия, создаст обработчик и соответствующий поток, который вы затем можете назначить событиям React, таким как onChange, onClick и т. Д. Значения из этого события будут затем переданы в поток, который вы затем можете передать по конвейеру, сопоставить , и все, что вам нужно. В нашем случае мы хотим сгладить и входящий поток props, и поток от события и создать один объект.

Взяв наши реквизиты, мы хотим проверить, есть ли пользователь, указанный в URL-адресе. В этом проекте используется react-router-dom, поэтому мы находим параметр пользователя в props.match.params.user. Но где Route передать опору match этому потоку?!? Не волнуйтесь, мы доберемся до этого, просто знайте, что на данный момент selectUser будет передаваться реквизит из компонента Route response-router. Теперь, когда у нас есть пользователь из URL-адреса, мы хотим запустить наш поток либо с этим пользователем, либо с первым пользователем в нашем userList. Если у нас еще нет userList, мы просто хотим начать с null. Затем мы просто пропускаем selectedUser через поток и возвращаем объект, который содержит все реквизиты, переданные в selectUser, а также нашу функцию userSelect.

Чтобы использовать только что созданный поток с двумя опорами, нам нужно создать компонент, который будет принимать реквизиты из обоих потоков и отображать наш элемент списка.

Сам компонент довольно прост, он просто берет реквизиты, проверяет, указан ли нужный пользователь в URL-адресе, а если нет, подталкивает туда нужного пользователя. Затем он берет наш userList, отображает его и возвращает элемент списка для каждого пользователя в списке и добавляет некоторые условные стили, если пользователь в данный момент выбран. Если запрос все еще продолжается, он отображает компонент загрузки. Обратите внимание, что мы передаем функцию userSelect элементам нашего списка для использования в качестве обработчика onClick. Пользователь, переданный в userSelect, затем помещается в поток selectUser.

Чтобы у этого компонента был доступ ко всем этим свойствам, пришло время для некоторой красоты функционального программирования. Используя функцию compose из Recompose, мы создаем новый элемент, который передает все реквизиты как из load, selectUser, так и из response-router в элемент IndexPage. Здесь мы также создадим функцию setUserlist и опору userList, которые мы видели в нашем потоке load.

Функции withState и withHandlers позволяют нам хранить постоянные данные и манипулировать этими данными внутри наших потоков. withState принимает три аргумента: имя элемента в состоянии, имя функции, используемой для обновления состояния, и начальное состояние. withHandlers получает функцию, указанную в withState, и может принимать столько обработчиков, сколько вам нужно. В нашем случае нам нужен только один обработчик, который возьмет массив users из нашей функции выборки и установит свойство состояния userList для этого массива. Мы также передаем функцию withRouter, чтобы наши компоненты и потоки событий имели доступ к свойствам маршрутизатора, таким как match и history.

Пора делать первые шаги

Следующие шаги

Теперь, когда у нас есть список пользователей, пришло время создать компонент, который будет отображать их списки симпатий и антипатий. Здесь мы можем воспользоваться дополнительными вспомогательными функциями перекомпоновки withContext и getContext. Это предохранит нас от попадания в ад из унесенных реквизитом. Поскольку мы будем создавать больше обработчиков для обновления списка пользователей, будет много функций, которые мы захотим передать нашим компонентам, которые отображают симпатии и антипатии, что может привести к примерно следующему:

<InfoTables {...{addLike, addDislike, changeLike, changeDislike, deleteLike, deleteDislike, selectedUser}} />

Затем все эти реквизиты нужно будет постоянно передавать, пока они не достигнут компонентов, которые их фактически потребляют. Это безумно! Используя withContext и getContext, мы можем передавать необходимые нам свойства и использовать их в любом дочернем элементе нашего IndexPage компонента. Итак, давайте создадим наши обработчики:

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

withContext принимает два аргумента, первый - это childContextTypes, а второй - функция, которая возвращает реквизиты, которые должны быть переданы как контекст. В нашем случае мы хотим передать выбранного пользователя и функции нашего обработчика состояния в качестве контекста. Мы получаем эти свойства, помещая нашу withContext функцию в качестве аргумента нашей compose функции следующим образом:

Теперь мы можем использовать эти реквизиты как контекст в любом дочернем компоненте!

Использование контекста в дочерних компонентах

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

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

Пришло время создать больше потоков, управляемых событиями DOM. Первый поток, который мы хотим создать, будет потоком для открытия и закрытия модальной формы:

Обработка события переключения довольно проста: мы указываем, должен ли переключатель быть включен или выключен, а затем используем scan - функцию, которая применяет аккумулятор к потоку (аналогично Array.reduce) - для переключения состояния. Затем мы возвращаем все переданные свойства, а также текущее состояние модального окна и самого обработчика.

Работа с вводом текста немного сложнее:

В приведенном выше фрагменте мы берем поток, созданный нашим обработчиком событий (в данном случае ввод текста), и создаем новый поток, состоящий только из значения event.target. Также важно отметить, что мы должны использовать здесь startWith, иначе этот поток не будет подписан, и наш компонент, который использует этот поток, не будет отображаться.

Чтобы получить доступ к этим функциям в нашем компоненте List, мы должны составить их вместе следующим образом:

Используя составные потоки, мы можем создать красивую модальную форму, которая принимает текстовый ввод и добавляет его в список лайков пользователя. Вот его родительский компонент:

И сам модал:

То же самое и с антипатиями пользователя дает нам два списка, которые пользователь может изменять.

Заворачивать

Наблюдаемые потоки - отличный способ управления потоками данных в компонентах. Хотя некоторые из описанных здесь вещей могут показаться излишними, я хотел показать, что составные потоки хорошо работают во многих случаях использования. Разделение логики на потоки может помочь создать отдельные и составляемые части логики, которые могут возникать из сложных пользовательских интерфейсов. Компоненты, которые подписываются на изменения в потоках, могут реагировать на один источник истины, вместо того, чтобы проверять наличие побочных эффектов в таких методах жизненного цикла, как componentDidUpdate. Я думаю, что это отличный способ удалить некоторые антишаблоны, которые мы видим в компонентах React, и избежать затрат на производительность большого хранилища Redux.