Следующая остановка, Redux [Pt. 8.R – Hello Redux, my old friend]

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

Начнем со сложного, зачем вообще редакс?

Во-первых (но это неточно), для решения описанных в недавнем опусе проблем – передачи соостояния далеко вглубь дерева компонентов и распротранение его между братьями или даже несколькими ветвями дерева компонентов.

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

Вторая и как правило более серьезная причина это набор преимуществ подхода редакса: тестируемоесть, предсказуемость и доступная надежность Состояниия.

В-третьих и к сожалению – редакс это модно. Насколько я помню, он был одной из первых (или даже первой) вышедших библиотек, которые решали проблему Состояния в мире реакта. И редакс решил ее настолько хорошо, что стал практически де-факто Состоянием “настоящих” приложений на реакте. Как ни удивительно, это приносит свои плюсы и минусы, но о них как-нибудь в другой раз.
Главное не забывай, что Redux это не единственный выход, каким бы чертовским привлекательным он не выглядел.

Что есть Redux?

Как я много раз уже говорил – Redux это библиотека для управления состоянием. Она основывается на взаимосвязи единого дерева состояния (store), которое может быть изменено только за счет чистых (pure) функций-редукторов (reducer), получающих действия (action) описывающие какие изменения необходимы.

Сразу сноска:

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

Редукторы могут быть не чистыми функциями, но это ломает перемещение по состоянию, дебаггер, и вообще строго не рекомендуется.

Можно с легкостью проследить подобие такой стратегии подходу Flux – Действия (action) отправляются в Диспетчер (reducer?), который создает новое Состояние (store).
(Создатели редакса отрицают использование Диспетчера, и технически это правда, но размышлять полным набором элементов модели и их сутью удобнее и проще)

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

Поэтому перед продолжением повторю еще раз:
У нас есть одно состояние на все приложение – store,
Оно изменяется только через чистые функции – reducers,
Эти функции получают простые объекты-действия с указаниями какие изменения нужно внести – action.

Теоретически можно провести аналогию с базовым setState реакта:

// redux:
reduce((oldStore, action) => newStore ) -> newStore
// almost the same (on surface), react:
event => (action => this.setState(reduce(action))) -> newState

Только модель редакса предлагает куда более широкий доступ к самому процессу установки нового состояния.

Как?

Я уже много раз повторял, что в модели флукса, которой практически следует редакс, идет строгий порядок действий – (из Представления) создается Действие, оно попадает в Диспетчер который решает каким получится новое Состояние, из которого собирается Представление и цикл замыкается.

Как мы уже договорились – реакт является нашим Представлением – он собирает разметку из свойств (props) и опционально Состояния (state) компонента.

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

При этом редакс, являясь полной реализацией Состояния (как реакт являеется реализацией Представления), имеет также базовые задатки Действий и кхем Диспетчера (так же, как реакт имеет задатки для полной модели). Можно догадаться, что я веду к тому, что делая реализацию модели более устойчивой с полноценной библиотекой владеющей Состоянием, мы в будущем можем сделать тоже и со связкой Диспетчера+Действия.
(Намек на будущее это redux-saga)

Но на секунду перестанем заглядывать за занавеску времени и вернемся к редаксу и, пожалуй, наконец посмотрим на “реальный” код.

Наконец, демонстрация

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

@reducer.js
const types = {
    newAvatarSet: 'NewAvatarSet',
}
export const actions = {
    setNewAvatar: (avatarUrl) => ({
        type: types.newAvatarSet,
        avatarUrl,
    })
}
export const reducer = (state, action) {
    switch(action.type) {
        case types.newAvatarSet:
        const { avatarUrl } = action;
        return {
            ...state,
            avatar: avatarUrl,
        }
        default: return state;
    }
}

@App.jsx
import {actions} from './reducer';

const Profile = ({name, avatar, bio}) =>
<div>
    <div>
        <span>{name}</span>
        <img
            src={avatar}
            alt={name + "'s pretty face'"}
            onClick={
                actions.setNewAvatar(/* url returned from modal for example*/)
            }
        />
    </div>
    <p>{bio}</p>
</div>;

export default MagicallyConnectReduxToReactComponentAndReturnsComponentWithRequiredConnections(
    transferReduxStateToReactProfile,
    transferAvailableReduxActionsToReactProfile // как правило импортированный объект actions
)(Profile);

@index.js
const store = ReduxMagicStoreCreator(...);
ReactDOM.render(
<ReduxMagicWrapper store={store}>
    <App />
</MagicWrapper>
, DOMnode)

Итак, что у нас тут интересного:
– Новый файл reducer.js – по сути тут живет редакс, обычно название файла соответствует названию компонента либо ветви дерева Состояния, но на этот раз я для простоты оставил просто reducer.
– “Неведомый” store переданный “неведомому” ReduxMagicWrapper-у – это детали связи между реактом и редаксом, которые я обозрю капельку позже.
const actions = { action: (ourParameters) => ({type: ourActionType, ourParameters})}; – непосредственно объект действий. Как правило это будет объект с функциями, которые возвращают объекты Действий, иногда можно сделать “сложнее” и “круче” через функцию возвращающую функции, но это пока не наш случай.
const reducer = (currentState, recievedAction) => (newState) – “редуктор-присваиватель” – функция, которая получает текущее состояние приложения и придуманное нами действие и по придуманной нами логике создает новый лучший объект состояния. Я не зря выделил тот факт, что мы решаем как выглядят действия и что с ними делает редуктор, этот момент долго путал меня, когда я только начинал разбираться с редаксом.
– Кривой и непродуманный onClick у аватарки – когда я думал на примере чего показать редакс, я забыл что мне нужно будет как-то создавать Действие, считай что там крутая схемка вызывающая модальное окошко, в которое bla-bla-bla.

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

@types.js
export default {
    SET_NEW_AVATAR: 'SET_NEW_AVATAR',
}

@actions.js
import types from './types';
export default {
    setNewAvatar: (avatarUrl) => ({ type: types.SET_NEW_AVATAR, avatarUrl}),
}
// и редуктор в котором так же импортируются типы действий

Кому как, но меня такое немного пугает. Поэтому я со временем пришел к “своему” формату, который и буду использовать в примерах. Его отличия немногочисленны но приятны, о них и причинах перехода, впрочем чуть позже. Впрочем, работая в комманде скорее всего придется столкнуться с таким форматом – он бывает удобен и полезен, когда создается достаточно большое приложение и нужно хорошее разделение проблем.

Вернемся к интересному, а точнее к деталям работы.

Во-первых, мы оборачиваем наше приложение специальным компонентом, который создает и предоставляет контекст (апи реакта, о котором я упоминал) всему приложению ниже себя в дереве компонентов. В него нужно передать объект Состояния редакса, созданный через специальную функцию.
Специальная функция нужна для указания используемых редукторов (reducers), подключения расширений (middleware) и задания начальных данных Состояния (к примеру при загрузке сохраненных настроек либо при сервереном рендере, когда сервер отправил пользователю страничку вместе с Состоянием).

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

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

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

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

nil commento load