# Redux ## Зачем? Вы могли заметить, что **React** *не предоставляет* (но и не навязывает) способов работы со слоем данных и/или бэкэндом на фронте. Поток данных в **JSX** через `props` не подходит для этого, так как одна и та же информация с бэка (например о текущем залогиненном пользователе) может применяться в отображении во множестве компонентов разных размеров и глубины вложенности. Попытки передать такую информацию через `props` из главного компонента `App` привели бы к сильному загрязнению кода **JSX** всякими `props` с данными и множественной копипасте. ### Компоненты и хранилище. Если вы сделали ДЗ, то вы смогли организовать **AJAX** к бэку напрямую из компонентов. Для несложных проектов из малого количества компонентов этот подход имеет право на жизнь и неплохо ложится на жизненный путь компонента. Однако в более сложных интерфейсах однотипные запросы будут в разных компонентах, да и в целом *компоненты должны влиять друг на друга*. *Попытки* **напрямую** *общаться между компонентами* (например через колбэки или методы) приводят к **спагетти-коду**, так как "связей" в коде оказывается слишком много, и эта паутина сложно отлаживается и поддерживается в дальнейшем. Таким образом, нужен механизм общего хранилища информации о **состоянии всего приложения**, общедоступный для любого из компонентов, в независимости от его размера и глубины вложенности. При наличии подобной **общей точки** для обмена данными между бэком и компонентами (и компонентами между собой) количество связей между частями приложения *меньше* чем при прямом обращении компонент-компонент. **Связи** при наличии хранилища предполагают разделение ответственности: - Новые данные посылаются в хранилище потому что *так надо*; компонент, посылающий такое событие не волнует, как именно это изменение будет обработано. - Хранилище *знает*, как правильно обработать новые данные; Хранилищу *не важно*, **откуда** эти данные пришли, и куда уйдут; - Все компоненты, которые *подписаны* на этот вид данных, занимаются отображением и/или изменением логики своей работы, *не заморачиваясь с тем, откуда этот сигнал пришел*. Такой подход позволяет гибко настраивать и более прозрачно отслеживать потоки данных внутри приложения. ## Как? - Само хранилище реализуется объектом, запрещенным к изменению напрямую; - Запрос на изменение хранилища присылается в форме объекта (**action**), который обрабатывается специальной функцией-**редьюсером**. - Редьюсер создает **новый объект** хранилища и возвращает его. - Хранилище обновляется и *оповещает подписчиков*. **Итого**: минимальная реализация **redux** занимает строк 50. ## React Для связывания компонентов с **redux** используется подход **HOC** - **high-order components**, аналог функций высшего порядка, только для компонентов. Суть в том, что на базе вашего компонента создается компонент-обертка, которая передает в ваш компонент нужные части хранилища и функции для изменения хранилища используя `props`. Таким образом компонент *автоматически* отображает изменения в хранилище и имеет возможность послать запрос на изменение в хранилище. ## Redux Minimal ```bash npm install --save redux react-redux ``` ```jsx import {Provider, connect} from 'react-redux'; import {createStore, combineReducers} from 'redux'; let store = createStore((state, action) => { //единственный редьюсер данного хранилища if (state === undefined){ //redux запускает редьюсер хотя бы раз, что бы инициализировать хранилище return {counter: 0}; //обязательно вернуть новый объект, а не изменить текущий state } if (action.type === 'COUNTER_INC'){ //в каждом action должен быть type return {counter: state.counter +1} //создаем новый объект базируясь на данных из предыдущего состояния } if (action.type === 'COUNTER_DEC'){ return {counter: state.counter -1} } return state; //редьюсеров может быть несколько, в таком случае вызываются все редьюсеры, но далеко не всегда action.type будет относится к этому редьюсеру. Тогда редьюсер должен вернуть state как есть. }) store.subscribe(()=> console.log(store.getState())) // подписка на обновления store store.dispatch({ type: 'COUNTER_INC' }) store.dispatch({ type: 'COUNTER_DEC' }) ``` - `createStore` создаёт новое хранилище. В качестве параметра передается *одна функция-редьюсер*, которая обрабатывает все запросы на изменение хранилища - **Редьюсер** принимает текущее состояние хранилища и объект `action` - действие над хранилищем. - Когда запускается `createStore` редьюсер запускается с `state = undefined` для первоначальной инициализации хранилища - В `action` обязательно должно быть поле `type` - **Редьюсер** обязан возвращать каждый раз *новый объект*, а не модифицировать старый. - Если редьюсер не знает переданный тип действия, он должен вернуть `state` как есть. - метод `subscribe` позволяет добавить любое количество колбэков, которые будут вызваны после изменения хранилища - метод `dispatch` посылает **объект-действие** в хранилище для обработки **редьюсером** ## Redux usual. В обычной ситуации один редьюсер с большим количеством действий смотрится не очень. Посему в комплекте с **redux** идет функция `combineReducers`, которая: - воспринимает не один редьюсер (как `createStore`), а объект, ключами в котором являются ветви хранилища, а значениями - редьюсеры, работающие с этой ветвью. - Редьюсеры получают не весь `state`, а только свою ветвь. - Редьюсер возвращает не весь `state`, а только свою ветвь. - Однако редьюсеры получают **все** действия: и свои, и чужие. Посему не забываем возвращать `state` неизменным если действие не относится к этому редьюсеру. ```jsx import {Provider, connect} from 'react-redux'; import {createStore, combineReducers} from 'redux'; let counterReducer = (state, action) => { //один из редьюсеров данного хранилища if (state === undefined){ //redux запускает редьюсер хотя бы раз, что бы инициализировать хранилище return {counter: 0}; //обязательно вернуть новый объект, а не изменить текущий state } if (action.type === 'COUNTER_INC'){ //в каждом action должен быть type return {counter: state.counter +1} //создаем новый объект базируясь на данных из предыдущего состояния } if (action.type === 'COUNTER_DEC'){ return {counter: state.counter -1} } return state; //редьюсеров может быть несколько, в таком случае вызываются все редьюсеры, но далеко не всегда action.type будет относится к этому редьюсеру. Тогда редьюсер должен вернуть state как есть. } let booleanReducer = (state, action) => { //второй редьюсер if (state === undefined){ return {boolean: false} } if (action.type === 'BOOLEAN_SET'){ return {boolean: true} } if (action.type === 'BOOLEAN_RESET'){ return {boolean: false} } if (action.type === 'BOOLEAN_TOGGLE'){ return {boolean: !state.boolean} } return state; //редьюсеров может быть несколько, в таком случае вызываются все редьюсеры, но далеко не всегда action.type будет относится к этому редьюсеру. Тогда редьюсер должен вернуть state как есть. } const reducers = combineReducers({ //создаем функцию-обертку, которая запустит последовательно counterReducer и booleanReducer передав им ветви c и b хранилища и обновив эти же ветви в случае нового состояния. c: counterReducer, b: booleanReducer }) const store = createStore(reducers); store.subscribe(()=> console.log(store.getState())) // подписка на обновления store, теперь тут две ветви. store.dispatch({ type: 'COUNTER_INC' }) store.dispatch({ type: 'BOOLEAN_SET' }) store.dispatch({ type: 'COUNTER_DEC' }) store.dispatch({ type: 'BOOLEAN_TOGGLE' }) ``` Как видно из примера выше, редьюсер, когда он один, и редьюсер, который передается одним из многих в `combineReducers` являются одинаковыми. Единственное отличие в том, что для `combineReducers` обязательно быть готовым что ни один из if не отработает, так как тип действия будет для другого редьюсера. В случае с одним редьюсером это не важно. ## actionCreators По феншую вы не отправляете объект действия непосредственно литеральным параметром `dispatch`, а создаете функцию, которая на базе нужной информации возвращает объект действия. Это удобно при работе с компонентами **React**. ```jsx function actionInc(){ return { type: 'COUNTER_INC' } } function actionDec(){ return { type: 'COUNTER_DEC' } } store.dispatch(actionInc()) store.dispatch(actionDec()) ``` ## React-Redux Компоненты связываются с **redux** гибким способом: - Все компоненты, которые вы хотите присоединить к тому или иному хранилищу должны находится внутри ``. Таким образом вы можете переподключить все вложенные компоненты к другому хранилищу в одном месте. Хотя обычно предполагается что хранилище одно на приложение. - При создании компонента-обертки, связанного с **redux** вы можете указать какие части хранилища подключить к каким `props`; а так же передать через `props` набор функций, создающих действия (actionCreator), для отправки данных из компонента в хранилище. Таким образом: - Всегда можно поменять `store` в `` и переподключить компонент на совершенно другое хранилище - Всегда можно отладить ваш компонент просто подсовывая ему литерально `props`, таким образом просимулировав работу с **redux**. ```jsx class Counter extends Component{ render(){ return (
{this.props.counter}
); } } let mapStateToProps = state => ({counter: state.c.counter}) //функция должна из state достать нужное значение, которое попадет в props let mapDispatchToProps = {actionInc, actionDec}; //actionCreator-ы, тут переданные, автоматом оборачиваются в dispatch let ConnectedCounter = connect(mapStateToProps,mapDispatchToProps)(Counter) //connect возвращает функцию, которая из любого компонента сделает компонент с props из mapStateToProps и mapDispatchToProps let ConnectedViewCounter = connect(mapStateToProps)(props =>
{props.counter}
) class App extends Component { render() { let counter = 0; return (
console.log('тут бы inc и setProps')} actionDec={()=>console.log('тут бы dec и setProps')} />
); } } ``` Компонет `Counter` из примера выше готов принять через `props` 3 параметра - функции для увеличения и уменьшения счетчика, и само значение счетчика. Если его использовать напрямую, то каждый раз придется эти параметры явно указывать, что и проиллюстрировано. Используя `connect` можно получить компонент-обертку (тут этот класс хранится в переменной `ConnectedCounter`), который будет использовать ваш компонент, но передавать ему нужные данные из `store` и обеспечивать возможностью отправить данные в `store`. ### `connect` - Первый параметр - функция, которая собирает нужную информацию из хранилища (в функцию передается текущее состояние хранилища) и возвращает объект с ключами, которые вам нужны в `props`.` - Второй параметр - объект, в котором ключами будут опять же свойства в `props`, а значениями - actionCreator-ы. `connect` автоматически обернет ваши actionCreator в `dispatch`, после чего можно спокойно вызывать `this.props.actionName()` - это вызовет `store.dispatch(actionName())` **connect возвращает функцию, которая из любого компонента сделает компонент-обертку с вышеуказанными настройками**. На самом деле `connect` более всеяден в плане параметров, но вы всегда можете это загуглить. #### `ConnectedViewCounter` Данный компонент сделан прямо на месте (литерально). Функции-результату `connect` передается не имя класса, а компонент-функция. Данный компонент иллюстрирует, что теперь два компонента на странице связаны с общими данными и работают синхронно, не зная друг о друге. ## Почитать https://getinstance.info/articles/react/learning-react-redux/