Вы могли заметить, что React не предоставляет (но и не навязывает) способов работы со
слоем данных и/или бэкэндом на фронте. Поток данных в JSX через props
не подходит для этого,
так как одна и та же информация с бэка (например о текущем залогиненном пользователе) может применяться
в отображении во множестве компонентов разных размеров и глубины вложенности. Попытки передать
такую информацию через props
из главного компонента App
привели бы к сильному загрязнению
кода JSX всякими props
с данными и множественной копипасте.
Если вы сделали ДЗ, то вы смогли организовать AJAX к бэку напрямую из компонентов. Для несложных проектов из малого количества компонентов этот подход имеет право на жизнь и неплохо ложится на жизненный путь компонента. Однако в более сложных интерфейсах однотипные запросы будут в разных компонентах, да и в целом компоненты должны влиять друг на друга.
Попытки напрямую общаться между компонентами (например через колбэки или методы) приводят к спагетти-коду, так как "связей" в коде оказывается слишком много, и эта паутина сложно отлаживается и поддерживается в дальнейшем.
Таким образом, нужен механизм общего хранилища информации о состоянии всего приложения, общедоступный для любого из компонентов, в независимости от его размера и глубины вложенности.
При наличии подобной общей точки для обмена данными между бэком и компонентами (и компонентами между собой) количество связей между частями приложения меньше чем при прямом обращении компонент-компонент.
Связи при наличии хранилища предполагают разделение ответственности:
Такой подход позволяет гибко настраивать и более прозрачно отслеживать потоки данных внутри приложения.
Итого: минимальная реализация redux занимает строк 50.
Для связывания компонентов с redux используется подход HOC - high-order components, аналог
функций высшего порядка, только для компонентов. Суть в том, что на базе вашего компонента
создается компонент-обертка, которая передает в ваш компонент нужные части хранилища
и функции для изменения хранилища используя props
. Таким образом компонент
автоматически отображает изменения в хранилище и имеет возможность послать
запрос на изменение в хранилище.
npm install --save redux react-redux
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 идет функция
combineReducers
, которая:
createStore
), а объект, ключами в котором являются ветви хранилища, а значениями - редьюсеры, работающие с этой ветвью.state
, а только свою ветвь.state
, а только свою ветвь.state
неизменным если действие не относится к этому редьюсеру.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 не отработает, так как тип действия будет для другого редьюсера. В случае с одним редьюсером это не важно.
По феншую вы не отправляете объект действия непосредственно литеральным параметром dispatch
, а создаете функцию, которая на базе нужной информации
возвращает объект действия. Это удобно при работе с компонентами React.
function actionInc(){
return {
type: 'COUNTER_INC'
}
}
function actionDec(){
return {
type: 'COUNTER_DEC'
}
}
store.dispatch(actionInc())
store.dispatch(actionDec())
Компоненты связываются с redux гибким способом:
<Provider store = { someStore } />
. Таким образом вы можете переподключить все вложенные
компоненты к другому хранилищу в одном месте. Хотя обычно предполагается что хранилище одно
на приложение.props
; а так же передать через props
набор
функций, создающих действия (actionCreator), для отправки данных из компонента в хранилище.Таким образом:
store
в <Provider />
и переподключить компонент на совершенно другое
хранилищеprops
, таким
образом просимулировав работу с redux.class Counter extends Component{
render(){
return (
<div>
<button onClick={this.props.actionInc}>+</button>
<span>{this.props.counter}</span>
<button onClick={this.props.actionDec}>-</button>
</div>
);
}
}
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 => <div>{props.counter}</div>)
class App extends Component {
render() {
let counter = 0;
return (
<Provider store = {store} >
<div className="App">
<Counter counter={counter}
actionInc={()=>console.log('тут бы inc и setProps')}
actionDec={()=>console.log('тут бы dec и setProps')}
/>
<ConnectedCounter />
<ConnectedViewCounter />
</div>
</Provider>
);
}
}
Компонет 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/