Просто чтобы хранить данные, менять, подписываться на изменение и доставать откуда угодно.
API:
- createStore(reducer) - создать store
- store.dispatch(action) - отправить экшен
- store.subscribe(listener) - подписаться на изменения стейта
- store.getState() - доставать
const { createStore } = Redux;
// редьюсер описывает все возможные изменения стора, здесь стейт просто Number, может быть что угодно
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
// создаем стор
let store = createStore(counter)
// подписываемся
store.subscribe(() =>
console.log(store.getState())
)
// используем
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
https://codepen.io/anon/pen/NMPdbp
Для того чтобы все подряд не передавать придумали формат: https://github.com/redux-utilities/flux-standard-action
{
type, // тип, чтобы редьюсер знал как обработать
payload, // нагрузка, данные
error, // есть ли ошибка, хз не испльзовал
meta // доп инфа, хз не испльзовал
}
Ну и чтобы не писать store.dispatch({ type: 'INCREMENT' });
Придумали создавать функции - они называются actionCreators т.к. создают простые объекты экшенов:
function increment() {
return { type: 'INCREMENT' }; // возвращает простой объект
}
store.dispatch(increment())
Умеем хранить данные, менять их откуда угодно, читать, подписываться на изменения - надо как то связать с React.
// сначала все тоже самое
const { createStore } = Redux;
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
let store = createStore(counter)
class App extends React.Component {
state = {
counter: store.getState()
}
componentDidMount = () => {
// после маунта подписываемся на изменение стора, в подписке сетим стейт в counter (дублируем в компоненте), this.setState перерендеривает компонент с новым значением
store.subscribe(() => this.setState({ counter: store.getState() }));
}
increment = (event) => {
store.dispatch({ type: 'INCREMENT' });
}
decrement = (event) => {
store.dispatch({ type: 'DECREMENT' });
}
render() {
return (
<div>
{this.state.counter}
<button onClick={this.decrement}>-</button>
<button onClick={this.increment}>+</button>
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById("app")
);
https://codepen.io/anon/pen/dePNpB
Подключили реакт. Теперь можно любой компонент, где бы он не находился в иерархи дерева компонентов, подписать на глобальный стор таким способом.
Но такой способ слишком сложный и не много функций у него. Написали для таких подключений либу react-redux.
API:
- <Provider store={store}> - оборачивает все прирложение и в context запихивает туда store
- connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) - достает из контекста store, подписывается на него, мапит state на то, что из него нужно достать для конкретного компонента, connect это HOC
https://reactjs.org/docs/higher-order-components.html https://reactjs.org/docs/context.html теперь не нужно везде импортить store, чтобы подписаться или кинуть экшен. Уже все подписано - меняется стор и сразу перерендеривается все приложение.
Тот же пример с connect:
// все тоже самое
const { createStore, bindActionCreators } = Redux;
const { Provider, connect } = ReactRedux;
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
let store = createStore(counter);
class App extends React.Component {
// уже нет никаких подписок, все подписано в родителе AppContainer
render() {
const { counter, increment, decrement } = this.props; // просто принимаем что пришло из родителя и используем
return (
<div>
{counter}
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
)
}
}
// теперь все что касается подписок здесь AppContainer хранит дубликат стейта
const AppContainer = connect( // коннект примет стор, который ему передаст Provider и все сделает
function mapStateToProps(state, props) { // мапим весь стейт на то что нужно этому компоненту
return {
counter: state
}
},
function mapDispatchToProps(dispatch) { // мапим все экшены на то что нужно этому компоненту
return {
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' })
};
}
)(App);
ReactDOM.render(
<Provider store={store}> // Provider передаст стор в context чтобы connect смог его принять и сделать все подписки и достать все данные для конкретного компонента
<AppContainer />
</Provider>,
document.getElementById("app")
);
https://codepen.io/anon/pen/bMNgzY
Теперь по всему приложению можно коннектить любой компонент к любой части стора и менять из любого компонента любую часть стора.
Сейчас асинхронную логику можно писать внутри компонентов. Например при обработке клика. Или на componentDidMount и т.д.
<button onClick={() => getData().then(() => this.props.increment())}> // increment отарботает только после получения данных
Получить данные
</button>
Чтобы вынести подобную логику из компонентов придумали redux-thunk. https://github.com/gaearon/redux-thunk
теперь можно писать экшены как функции, которые принимают dispatch и getState установка: https://github.com/gaearon/redux-thunk#installation
пример таких экшенов:
function fetchUser(id) {
return (dispatch, getState, api) => { // теперь не просто объект, теперь функция, есть dispatch, теперь прямо из экшена можно диспатчить и полчать стор
fetchUsersApi(getState().userData.userId).then((response) => {
dispatch(setUsers(response.data.users)); // после получения данных можно вызвать что угодно, напрмер записать юзеров в стор, чтобы отобразить гдето в компоненте
// передается getState как функция потому, что тут все асинхронно и в этот момент (после получения данных) getState вернет актуальный state т.к. он уже мог поменятся
// можно еще задиспатчить что нибудь тут
dispatch(showNotification({ text: 'users fetched' }));
})
}
}
function setUsers(users) {
return { type: 'SET_USERS', payload: users };
}
Для того чтобы все изменения не писать в одной функции придумали комбинировать несколько функций, каждая из которых отвечает за свой кусок стейта
Например есть стейт в котором есть userData и pageData { userData: { name }, pageData: { title } }
и редьюсер:
function reducer(state, action) {
if (action.type == 'SET_NAME') {
return { ...state, userData: { ...state.userData, { name: action.payload}}};
}
if (action.type == 'SET_TITLE') {
return { ...state, pageData: { ...state.pageData, { title: action.payload}}};
}
}
слишком много писать для доступа к вложенным частям и не прикольно, что все в одной функции
можно переписать:
function userDataReducer(state, action) {
if (action.type == 'SET_NAME') {
return { ...state, name: action.payload }; // рачитываем на то, что в state придет userData
}
}
function pageDataReducer(state, action) {
if (action.type == 'SET_TITLE') {
return { ...state, title: action.payload }; // рачитываем на то, что в state придет pageData
}
}
// и кобинируем
var reducer = combineReducers({
userData: userDataReducer,
pageData: pageDataReducer
});
и так можно с любым, сколько угодно вложенным куском стейта
пример:
const reducer = combineReducers({
main: combineReducers({
calculator,
dashboard
}),
user: combineReducers({
main: user,
auth,
registration,
settings: userSettings,
recovery
}),
navigation,
drawer,
searchScreen,
splashScreen,
settings
});