ZarahioN Presents

Answering why

Следующая остановка, 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), который получает созданные нами Действия и согласно нашим указаниям возвращает новое состояние (в том числе возвращая исходное состояние, если изменений не требуется).

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

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

Leave a Reply

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