ZarahioN Presents

Answering why

Возвращения тавтологии [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!

Leave a Reply

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