ZarahioN Presents

Answering why

React-Redux

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

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

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

Возвращения тавтологии [Pt. 9.R — The Reduh deal]

Уф..

It’s been a long time, isn’t it?

Много всего произошло за.. месяц?..

Я искал работу..

Баловался нервишками..

Старательно избегал любых напоминаний о обещанном примере использования редакса.

А еще встретил Vue, но об этом интересном и увлекательном событии позжее.

А сейчас
*пуф-пуф пыльку с репо для редакса*

На чем мы там остановились? А-а, тудушковое приложение в классическом реакте. Ну, почти классическом, за исключением Immutable.js и редакс-оватого подхода к данным.

Вспомнить бы еще об чем я там раньше рассказывал. Да, точно, но для начала, или продолжения, на всякий случай в общем:

yarn add redux react-redux
или
npm i -S redux react-redux

Что добавит сам редакс и связующую библиотеку react-redux. Последняя нужна для упрощения подключения редакса к компонентам реакта и приложения, что технически можно и ручками, но не стоит.

Для начала

идет чутка всеми любимого и в меру единого конфигурационного кода в нашем src/index.js:

import { createStore, compose, combineReducers } from 'redux'
import { Provider } from 'react-redux'

import todoReducer from './TodoApp/reducer'
const composer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
    combineReducers({todoReducer}),
    composer()
)

ReactDOM.render(
<Provider store={store}>
    <App />
</Provider>
, document.getElementById('root'));

Как и всегда, идем по строкам:

1, 2
импорты, все привычно и знакомо.

4
мы импортируем редуктор (reducer) из созданного файла reducer.js, для простых приложений это как правило подходит, однако я расскажу про более привычный способ импорта позжее.

5
это специальный код для упрощения разработки, он говорит редаксу проверить есть ли в браузере специальное расширение Redux Dev Tools, которое я строго рекомендую поставить для комфорта первых шагов в редакс, и если оно имеется то использовать его функцию для подключения дополнений к редаксу (называемых middleware). Специальность функции заключается в том, что она автоматически подключает свое дополнение-middleware прямо из расширения. О самих дополнениях опять же позжее.

6-9
непосредственно запуск редакса который заключается в создании его «магазина» store или как я чаще его называю Состояния. Внимательно, вся активность непосредственно с состоянием осуществляется функционалом библиотеки redux-а, а не ее оберткой-коннектором react-redux.

Самое просто состояние можно инициализировать просто передав в createStore одну функцию-редуктор:

createStore(
    (state = { count: 0} , action) => action.type === 'increment' ?
        { count: state.count + 1} :
        state
)

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

const store = createStore(combineReducers({
    todoReducer,
    b: anotherImportedReducer
}))

Создаст такое дерево состояния редакса:

{
    todoReducer: <todoReducer's initial state>
    b: <anotherImportedReducer's initial state>
}

И наконец 11-15
Provider это специальный компонент подключающий переданное состояние редакса (store) к нашему приложению, втихоря залезая в context чтобы позже можно было отдать его [состояние] указанным нами компонентам.

На всякий случай повторим что произошло:
1. Мы импортировали редуктор todoReducer.
2. Мы создали состояние, передав скомбинированный через composeReducers редуктор (либо просто передав сам редуктор createStore(todoApp, ...))
3. Создавая состояние мы также передали наше особое дополнение для разработки полученное из расширения Redux Dev Tools посредством пустого вызова названной функции composer.
4. С помощью специального компонента Provider из react-redux мы незаметно передали состояние в контекст, позволяя в будущем получить его в любом компоненте приложения (App).

Страшно, но совсем несложно.

Однако, что же такое редуктор?

Уверен в прошлый раз я вкратце рассказывал про редакс и его место в нашем приложении, как он работает и что делает, и бла-бла-бла. Но! Свои пояснения я представлял с сугубо теоритической точки, сейчас же мы рассмотрим практику взглянув на самый просто редуктор который не может (практически) ничего:

@TodoApp/reducer.js

const initState = {
    hello: 'world'
};

export const types = {

};

export default (state = initState, action) => {
    switch(action.type){

        default: return state;
    }
};

Как можно заметить, стандартный экспорт у нас в самом низу (то что мы импортировали в index.js как import todoRecuer from ./TodoApp/reducer) и представляет он из себя стрелочную функцию, по двум достаточно условным причинам:

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

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

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

Но вернемся к сбежавшему коду — у нас осталась строка с пустым объектом types — сюда мы будем помещать типы действий редакса — строк-указателей, которые говорят редуктору, по какому пути switch-а нужно обработать пришедший объект действия (если его вообще нужно обрабатывать а не вернуть default ничего не трогая).

Что же, осталась последняя остановка для подключения нашего очень умного и полезного редуктора к приложению, но перед этим вопрос:

А что мы будем там хранить?

Одна из интересных «проблем» с первым знакомством с редаксом это желание и часто наставление запихнуть в него ВСЁ. Начиная от логики приложения, допустим CRUD-а наших тудушек, до состояния наведения каждой сколь важной кнопки. И если первое скорее всего получит N-ный набор преимуществ от переезда в состояние редакса, со вторым и всеми последующими нужно быть осторожными — излишек данных в едином дереве редакса усложняет его восприятие в куда большей степени, чем небольшие кусочки визуальной и не только логики в отдельных компонентах.

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

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

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

Сделать это умный (но точнее сложный)

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

@UsersSelect.jsx
const UsersSelect = ({users, selectedUser, onChange}) =>
<select
    value={selectedUser}
    onChange={evt => onChange(evt.target.value)}
>
    { users.map(
        (user, index) => <option key={index} value={index}>{user}</option>
    )}
</select>

@Header.js
const Header = ({users = ['Tom', 'Peter']}) =>
<div className="todos">
    <UsersSelect users={users}/>
</div>

@TodoApp/index.js
render() {
    return (
        <div>
            <Header/>
            <TodoList
                setKey="today"
                todosLabel="ToDo ToDay"
            />
        </div>
    );
}

Пока все просто-мирно и знакомо, мы создали два функциональных компонента выводящие данные.

@Header.js
import { connect } from 'react-redux'
...
export default connect(
    function(state) { return state.todoReducer; }
)(Header);

И тут мы встречаем небезизвестный connect.

Что же это такое?

По факту connect это фабрика оберток, или Higher order Component(ов) — HoC. Она принимает до 4 аргументов, первый из которых функция-разбборщик состояния редакса, как правило ее называют mapStateToProps, потому что она, как следует из говорящего названия — привязывает состояния редакса с свойствам компонента.

Очень часто ее можно встретить в одном из трех максимально простых видов:

connect(state => ({
    propA: state.reducerA.propA, // ein
    wholeReducer: state.reducerB, // zwei
    ...state.destructuredReducer // drei
}))

Первый случай это прямое взятие одного значения из состояния редуктора reducerA в свойство propA,
второй — это взятие всего состояния редуктора reducerB в свойство wholeReducer,
а третий это деструктуризация всех значений из редуктора destructuredReducer.
Важное замечание: такой разбор подразумевает комбинирование редукторов через combineReducers который создает отдельную ветку под каждый редуктор, если создавать состояние редакса из одной функции-редуктора, то он будет являться непосредственно state-ом, но такое использование редакса достаточно редко.

Имея полученое знание, что получит наш Header с таким коннектом? Правильным ответом будет весь набор todoReducer-а — возвращенное из mapStateToProps значение по ключу передается свойствами в компонент, в этом можно самостоятельно убедиться глянув как будет передана простая строка.
А мы же можем проверить что базовое состояние нашего редуктора успешно передалось выведя значение свойства helloHello {hello} — где-либо в компоненте (не забудь добавить его в деструктуризацию из props либо получение его оттуда через (this.)props.hello).

Все это хорошо, но нужно закругляться и сделать редуктор не настолько глупым — зададим адекватное начальное состояние и добавим наш первый создатель действий «action creator«:

@reducer.js
const initState = {
    users: ['Tom', 'Peter', 'Vanko', 'Monkeyman'],
    selectedUserId: 0,
};

export const types = {
    selectUser: 'SelectUser',
};
export const actions = {
    selectUser: (selectedUserId) => ({
        type: types.selectUser, selectedUserId
    })
};

export default (state = initState, action) => {
    switch(action.type){
        case types.selectUser:
        return {
            ...state,
            selectedUserId: action.selectedUserId,
        }
        default: return state;
    }
};

Как можно заметить наш редуктор пополнился, initState получил массив пользователей и переменную для выбранного пользователя. Помимо этого добавилось значение в объекте types, которое используется в самом редукторе, чтобы «словить» нужное действие и в новом объекте actions, чтобы это действие создать.

Объект actions является по сути списком методов, возвращающих объекты действий — поэтому их и называют action-creators. Каждый метод должен возвращать простой объект, который должен содержать по крайней мере type (который может быть пустым, но так делать не стоит, мы же хотим знать что нужно сделать редуктору и различать пришедшие действия).

Создатели действий это хорошо, но мы же хотим использовать их в своих компонентах? Для этого в функции connect есть второй аргумент, он принимает объект методов\функций возвращающих объекты-действия — наших создателей действий. (Меня всегда радует количество тавтологии при объяснении редакса.)

@Header.js
import { actions } from './reducer';
...
export default connect(
    state => state.todoReducer,
    actions,
)(Header);

Как можно заметить, мы банально передали описанный в самом редукторе объект actions. При желании можно записать действия отдельным объектом или напрямую параметром в коннект:

const mapDispatchToProps = {
    selectUserId() {
        return { type: importedTypes.selectUser }
    }
}
connect(..., mapDispatchToProps)
// or
connect(..., {
    selectUser: () => ({ type: importedTypes.selectUser })
})

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

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

@Header.js
const Header = ({ users, selectedUserId, selectUser }) =>
<div className="todos">
    <UsersSelect
        users={users}
        selectedUserId={selectedUserId}
        onChange={selectUser}
    />
</div>

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

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

Однако отвечая на вопрос — для будущего.

Помнишь о моем извращеном примере с аватарками и связями? Примерно такой но в более скромном масштабе я планирую показать в ближайшем будущем, не учить же людей как надо делать, не так ли? Однако, для подхода к такому ответственному баловству нужна еще пара моментов, на разбор которых у нас не осталось времени. reselect, hint-hint

А пока, наше любимое повторение:
— В Header.js мы ввели состояние редуктора todoApp с помощью connect(state => state.todoApp, ...)
— В Header.js мы ввели создатели действий для редуктора todoApp с помощью connect(..., actions)
— В Header.js мы привязали полученые свойства и метод для обновления выбранного пользователя в состоянии редакса

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

И на этом на сегодня все, прощаюсь не прощаясь.
See ya!

Welp, we’ve tried Reduh way of things [Pt. Fin]

So, я пытался рассказать о реакте, редуксе, и прочих прелестях.

Но, я встретил его, Vue, и.. все очень быстро поменялось.

Он настолько простой, приятный и «легкий», что возвращаться к громоздкому реакту слишком сложно.

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

So in so, fare thee well.