# Redux, менеджмент состояний
## Зачем нужен Redux Toolkit?
- Упрощает подключение redux к React
- Снижает количество дублированного кода
## Обзор redux, react-redux, redux toolkit - что и зачем
- redux - библиотека для управления состоянием приложения
- react-redux - библиотека для подключения redux к react
- redux-toolkit - набор инструментов для сокращения дублей кода и упрощения файловой структуры проекта
## Отличия и основы пакетов
### redux
- createStore - создаёт хранилище
- store.dispatch - отправляет действие в диспетчер
### react-redux
- Provider - провайдер хранилища. Компонент React. Делает хранилище доступным для других компонентов
- useSelector - позволяет по переданной функции получить значение из хранилища
- useDispatch - позволяет обратиться к методу dispatch хранилища
Провайдер подключается обёрткой над приложением:
```jsx
import { Provider } from 'react-redux';
import { store } from './app/store';
// ...
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
```
Далее в компонентах:
```jsx
const dispatch = useDispatch();
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
```
### redux toolkit
- configureStore() - позволяет создать экземпляр хранилища через объект настроек
- createReducer() - создаёт редуктор
- createAction() - Создаёт Action Creator,
- createSlice() - создаёт объект, объединяющий по смыслу редуктор и действия для последующего импорта
- createAsyncThunk() - создаёт асинхронное действие (использует пакет redux-thunk)
## Обзор React Toolkit
Данный набор инструментов есть в составе стандартного create-react-app приложения с использованием шаблона redux
Установить его можно так:
```sh
npx create-react-app my-app --template redux
```
Или отдельно через npm:
```sh
npm install @reduxjs/toolkit
```
yarn:
```sh
yarn add @reduxjs/toolkit
```
## Подключение хранилища
С помощью configureStore возможно создать хранилище через единый объект
```javascript
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
```
## createSlice
```jsx
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
value: 0,
from: null,
to: null,
amount: 100,
}
const getValue = state => {
const { amount, from, to, value } = state;
if (!from || !to || !amount) {
return value;
}
const rate = from.rate / to.rate;
return amount / rate;
}
const converterSlice = createSlice({
name: 'converter',
initialState,
reducers: {
setFromCurrency(state, action) {
state.from = action.payload;
state.value = getValue(state);
},
setToCurrency(state, action) {
state.to = action.payload;
state.value = getValue(state);
},
setAmount(state, action) {
state.amount = action.payload;
state.value = getValue(state);
}
}
})
export const { setAmount, setFromCurrency, setToCurrency } = converterSlice.actions;
export default converterSlice.reducer;
```
Впоследствие converterSlice.reducer можно использовать как обычный редуктор и подключить к хранилищу
Использование в компоненте:
```jsx
import { CurrencySelect } from '../../';
import { Input } from 'antd';
import { useEffect, useState } from 'react';
import { CURRENCY_FROM, CURRENCY_TO } from '../../../constants';
import styled from 'styled-components';
const Container = styled.div`
max-width: 400px;
`;
export const Converter = ({ currencies = [], ...restProps}) => {
const currency = currencies[0]?.currency;
console.log(currency);
const [fromCurrency, setFromCurrency] = useState(currency);
const [toCurrency, setToCurrency] = useState(currency);
const [amount, setAmount] = useState(100);
const [result, setResult] = useState(0);
useEffect(() => {
console.log({
amount, fromCurrency, toCurrency,
});
if (!amount || !fromCurrency || !toCurrency) {
return;
}
const { rate: fromRate } = currencies.find(({ currency }) => currency === fromCurrency);
const { rate: toRate } = currencies.find(({ currency }) => currency === toCurrency);
const rate = fromRate / toRate;
setResult(amount / rate);
}, [amount, fromCurrency, toCurrency]);
const setCurrency = (type, value) => {
// console.log(value)
if (type === CURRENCY_FROM) {
setFromCurrency(value);
}
if (type === CURRENCY_TO) {
setToCurrency(value);
}
};
const onAmountChange = e => {
const value = +e.target.value;
setAmount(value);
}
return (
<Container {...restProps}>
<div>
<CurrencySelect
data={currencies}
label="Из"
defaultValue={fromCurrency}
onSelect={value => setCurrency(CURRENCY_FROM, value)}/>
</div>
<div>
<CurrencySelect
data={currencies}
label="В"
defaultValue={toCurrency}
onSelect={value => setCurrency(CURRENCY_TO, value)}/>
</div>
<div>
<Input type="number" placeholder="Сколько обменять" onInput={onAmountChange} defaultValue={amount}/>
</div>
<div>
<Input readOnly={true} type="number" placeholder="Сколько вы получите" value={result}/>
</div>
</Container>
);
};
```
## Вспоминаем шаблон наблюдатель
Шаблон проектирования состоит из 2 частей:
1. Издатель (publisher) - источник информации
2. Подписчик/наблюдатель (subscriber) - подписывается на изменения источника
Задача источника - оповестить каждого подписчика в случае, когда появятся обновления
```js
class Channel {
constructor() {
this.subscribers = [];
}
dispatch(message) {
this.subscribers.forEach(subscriber => subscriber.onMessage(message));
}
subscribe(subscriber) {
this.subscribers.push(subscriber);
}
}
class Subscriber {
constructor(name) {
this.name = name;
}
onMessage(message) {
console.log(`Пользователь ${this.name} получил сообщение: ${message}`)
}
}
const oleg = new Subscriber('Олег');
const ivan = new Subscriber('Степан');
const channel = new Channel;
channel.subscribe(oleg);
channel.subscribe(ivan);
channel.dispatch('Новое видео уже на канале!')
```
## Геттеры и сеттеры
Геттеры и сеттеры имитируют присвоение и получение значений из свойств объекта
```js
class User {
constructor(birthday) {
this.birthday = birthday;
}
get age() {
if (this._age) {
return this._age;
}
const now = new Date;
const diff = now.getTime() - this.birthday.getTime();
const YEAR = 365 * 24 * 60 * 60 * 1000;
const age = Math.floor(diff / YEAR);
this._age = age;
return this._age;
}
}
const birthday = new Date(1980, 03, 04);
const user = new User(birthday);
console.log(user.age);
console.log(user.age);
console.log(user.age);
console.log(user.age);
```
## Что такое MobX
Иной подход к централизованному управлению за состоянием
## MobX: терминология

1. Observable - объект, изменения которого отслеживаются. Обычно эту роль выполняет объект хранилища
2. Actions. Изменение значений в свойствах хранилища, а также вызов методов
3. Computeds - постоянно вычисляемые свойства, аналог геттеров в классе
4. Reactions - действия, выполняемые при изменении хранилища, в тч перерисовка представления
## Установка
```sh
npm i -S mobx
```
```sh
yarn add mobx
```
## Этапы
### Создать класс хранилища
Это обычный класс с методами и свойствами
```js
class TodoStore {
todos = [];
get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}
report() {
if (this.todos.length === 0)
return "<none>";
const nextTodo = this.todos.find(todo => todo.completed === false);
return `Next todo: "${nextTodo ? nextTodo.task : "<none>"}". ` +
`Progress: ${this.completedTodosCount}/${this.todos.length}`;
}
addTodo(task) {
this.todos.push({
task: task,
completed: false,
assignee: null
});
}
}
const todoStore = new TodoStore();
```
Для разделения по логике всего хранилища, можно создать несколько классов и экспортировать связь между ними:
```js
import { ConverterStore } from "./ConverterStore";
import { CurrenciesStore } from "./CurrenciesStore";
const { makeAutoObservable, makeObservable, observable } = require("mobx");
class RootStore {
converterStore;
currenciesStore;
constructor() {
this.converterStore = new ConverterStore(this);
this.currenciesStore = new CurrenciesStore(this);
}
}
class Store {
constructor() {
makeAutoObservable(this, {
rootStore: false
})
this.rootStore = new RootStore();
}
}
const store = new Store();
export default store.rootStore;
```
```js
import { makeAutoObservable } from "mobx";
export class ConverterStore {
from = null;
to = null;
amount = 100;
constructor(rootStore) {
makeAutoObservable(this);
this.rootStore = rootStore;
}
get value() {
const { amount } = this;
if (!this.from || !this.to || !amount) {
return 0;
}
const { currenciesStore } = this.rootStore;
const currencies = currenciesStore.value;
const from = currencies.find(({ currency}) => currency === this.from);
const to = currencies.find(({ currency}) => currency === this.to);
const rate = from.rate / to.rate;
return amount / rate;
}
}
```
```js
import { makeAutoObservable } from "mobx";
export const STATUS_INIT = 'STATUS_INIT';
export const STATUS_LOADING = 'STATUS_LOADING';
export const STATUS_DONE = 'STATUS_DONE';
export const STATUS_ERROR = 'STATUS_ERROR';
export class CurrenciesStore {
status = STATUS_INIT;
value = [];
constructor() {
makeAutoObservable(this);
}
addValues(values) {
this.value.push(...values);
}
}
```
### Включить отслеживание за объектами
Далее требуется в конструкторе класса хранилища включить отслеживание с помощью функций makeObservable или makeAutoObservable (пример выше):
```js
import { makeObservable } from "mobx";
class ObservableTodoStore {
todos = [];
pendingRequests = 0;
constructor() {
makeObservable(this, {
todos: observable,
pendingRequests: observable,
completedTodosCount: computed,
report: computed,
addTodo: action,
});
autorun(() => console.log(this.report));
}
get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}
get report() {
if (this.todos.length === 0)
return "<none>";
const nextTodo = this.todos.find(todo => todo.completed === false);
return `Next todo: "${nextTodo ? nextTodo.task : "<none>"}". ` +
`Progress: ${this.completedTodosCount}/${this.todos.length}`;
}
addTodo(task) {
this.todos.push({
task: task,
completed: false,
assignee: null
});
}
}
const observableTodoStore = new ObservableTodoStore();
```
## Подключение к React
mobx не использует какого-либо провайдера, поэтому реализация prop и хуков лежит на стороне разработчика
```jsx
import store from './store';
root.render(
<React.StrictMode>
<App store={store}/>
</React.StrictMode>
);
```
Для использование в компоненте используется пакет mobx-react
```jsx
import { CurrencySelect } from '../../';
import { Input } from 'antd';
import { runInAction } from 'mobx';
import { useEffect } from 'react';
import { CURRENCY_FROM, CURRENCY_TO } from '../../../constants';
import styled from 'styled-components';
import { observer } from 'mobx-react';
const Container = styled.div`
max-width: 400px;
`;
export const Converter = observer(({ currencies = [], store, ...restProps}) => {
const [firstCurrency] = currencies;
const fromCurrency = store.converterStore.from;
const toCurrency = store.converterStore.to;
const amount = store.converterStore.amount;
const result = store.converterStore.value;
useEffect(() => {
if (firstCurrency && (!fromCurrency && !toCurrency)) {
runInAction(() => {
store.converterStore.from = firstCurrency.currency;
store.converterStore.to = firstCurrency.currency;
})
}
}, [amount, fromCurrency, toCurrency, currencies, firstCurrency, store.converterStore]);
const setCurrency = (type, value) => {
// console.log(value)
if (type === CURRENCY_FROM) {
runInAction(() => {
store.converterStore.from = value;
})
}
if (type === CURRENCY_TO) {
runInAction(() => {
store.converterStore.to = value;
})
}
};
const onAmountChange = e => {
const value = +e.target.value;
runInAction(() => {
store.converterStore.amount = value;
});
}
return (
<Container {...restProps}>
<div>
<CurrencySelect
data={currencies}
label="Из"
defaultValue={fromCurrency}
onSelect={value => setCurrency(CURRENCY_FROM, value)}/>
</div>
<div>
<CurrencySelect
data={currencies}
label="В"
defaultValue={toCurrency}
onSelect={value => setCurrency(CURRENCY_TO, value)}/>
</div>
<div>
<Input type="number" placeholder="Сколько обменять" onInput={onAmountChange} defaultValue={amount}/>
</div>
<div>
<Input readOnly={true} type="number" placeholder="Сколько вы получите" value={result}/>
</div>
</Container>
);
});
```