Redux

Для простых приложений, таких как todo, используем один state, и передачу данных от него к каждому компоненту, который должен получить данные от state или может изменить их. 

При этом данные проходят через все компоненты, даже если им самим эти данные не нужны, но нужны их потомкам. Эта проблема называется Property Drill

Для более сложных приложений со слабо связанными компонентами возможно нахождение state в каждом компоненте. Но здесь серьёзная проблема, когда разработчик не учитывает наличие какой-то взаимосвязи компонентов, при этом приложение начинает работать неправильно, с ошибками. Эта проблема называется Fragmented State


Для больших приложений со сложными взаимосвязями между компонентами используем redux

При этом state выносится отдельно и есть функция reducer при помощи которой компоненты могут влиять на изменение state. События, которые влияют на состояние state называются Action

Store контролирует влияние reducer и изменение state 



Для работы с redux устанавливаем две библиотеки - redux и react-redux

Для этого в консоли выполняем команду

npm i redux react-redux

Запуск react-приложения выполняется командой

npm start

Функция-счётчик на чистом JS без redux

const reducer = (state = 0, action) => {
  if(action.type === 'INC') {
    return state += 1;
  } else {
    return state;
  }
}

let state = reducer(undefined, {}); // state = 0

state = reducer(state, {type: 'INC'});
console.log(state);

Таким образом
- reducer - функция, которая получает два аргумента - текущий state и action - действие, которое необходимо выполнить
- action - объект, у которого есть поле type. мы проверяем type и в зависимости от его значения выполняем указанную функцию
- если мы получаем action с неизвестным типом, возвращаем state без изменений
- если state = undefined, возвращаем значение state, которое указываем в значении параметра state функции reducer 

Ещё раз посмотрим на схему redux



Центральное место в redux занимает store
Основой store является функция reducer, которая отвечает за обновление state
Что касается самого state, его значение можно прописать в параметрах по умолчанию функции reduser. Если мы вызываем state с значением undefined, он принимает значение, указанное в параметрах reducer

Подключаем и используем redux

import { createStore } from 'redux';

const reducer = (state = 0, action) => {
  if(action.type === 'INC') {
    return state += 1;
  } else {
    return state;
  }
}

const store = createStore(reducer);
console.log(store.getState());

store.subscribe(() => {
  console.log(store.getState());
});

store.dispatch({type: 'INC'});
store.dispatch({type: 'INC'});

Что происходит в этом коде:
  • Импортируем функцию createStore из redux
  • Оставляем без изменений функцию reducer
  • Создаём переменную store из функции createStore с параметром reducer
  • Получаем первоначальное значение state
  • Добавляем слушатель store.subscribe, который будет выполнять указанную функцию, в данном случае store.getState() при изменении state
  • Обновляем значение state при помощи метода store.dispatch() параметром в который передаём указанный в reducer объект с типом action, который приводит к изменению store {type: 'INC'}
Функция reducer должна быть чистой функцией pure function

Для чистых функций должны выполняться два условия
  1. Результат выполнения зависит только от аргументов, при вызове функции с одинаковыми аргументами, результат всегда будет одним и тем же
  2. У функции нет никаких побочных эффектов
Поэтому в reducer мы не можем использовать время или генератор случайных чисел.
reducer работает только со своими параметрами - state и action. Он не может получать никакие другие аргументы, изменять значения переменных в коде, выводить данные в консоль или на страницу и т д.

Добавляем UI приложению-счётчику. Пока пишем его без react

Вёрстка
    <div id="root" class="jumbotron">
      <h1 id="counter">0</h1>
      <button class="btn btn-primary btn-lg" id="inc">INC</button>
      <button class="btn btn-primary btn-lg" id="dec">DEC</button>
    </div>
JS
const incBtn = document.getElementById('inc');
const decBtn = document.getElementById('dec');

incBtn.addEventListener('click', () => {
  store.dispatch({type: 'INC'})
});

decBtn.addEventListener('click', () => {
  store.dispatch({type: 'DEC'})
});

const update = () => {
  const counter = document.getElementById('counter');
  counter.innerHTML = store.getState();
}

store.subscribe(update);

Дополнительные параметры


Предположим, необходимо добавить в счётчик кнопку RANDOM, при клике по которой счётчик будет увеличивать store на рандомное значение от 0 до 9

Первая мысль как это сделать - написать в reducer следующее условие, и оно даже будет работать
if(action.type === 'RANDOM') {
    return state + Math.floor(Math.random() * 10);
  }
Но так делать не нужно.
Функция reducer - чистая функция, при таком условии её можно будет легко тестировать и выявлять баги.

Правильный вариант - создать переменную payload в параметрах функции dispatch (имя переменной произвольное, но типовое для дополнительных параметров)

randomBtn.addEventListener('click', () => {
  const payload = Math.floor(Math.random() * 10);
  store.dispatch({type: 'RANDOM', payload})
});

Условие в reduser будет выглядеть так

if(action.type === 'RANDOM') {
    return state + action.payload;
  }

Так как в метод dispatch передаём объект, дополнительных параметров может быть много, например

store.dispatch({
    type: 'LOGIN',
    name: 'Anton',
    role: 'admin'
  })


Action Creators


В реальных приложениях используются десятки действий и названия их могут быть очень длинными. При этом и писать их долго, и допустить ошибку просто

Для работы с ними используются функции  Action Creators, который немного упрощает код и делает его более читаемым

Создаём функцию, в которую передаём объект action
const inc = () => ({type: 'INC'});

и вызываем её в dispatch
store.dispatch(inc());

Если у метода dispatch есть дополнительный параметр, его передаём параметром функции action creator

const random = (payload) => ({type: 'RANDOM', payload});
store.dispatch(random(payload));

или

const userLoggedIn = (name, role) => ({ type: 'USER_LOGGED_IN', name, role });
store.dispatch(userLoggedin('Arnold', 'admin'));


Структура проекта


Вынесем  Action Creators и reducer в отдельные файлы actions.js и  reducer.js
В actions.js перед каждой функцией пишем ключевое слово export
export const inc = () => ({type: 'INC'});
В reducer.js используем export default reducer;

Импортируем их в index.js
import reducer from './reducer';
import {inc, dec, random} from './actions';

bindActionCreators()

Избавимся от store в выражениях вида
store.dispatch(random(payload));

Для этого используем деструктурирование
const {dispatch} = store;
dispatch(random(payload));

Вынесем функции из слушателей событий
Вместо
incBtn.addEventListener('click', () => {
  dispatch(inc());
});

Создадим функцию incDispatch и вызовем её в слушателе
const incDispatch = () => dispatch(inc());
incBtn.addEventListener('click', incDispatch);

Функция с дополнительным аргументом
const randomDispatch = (payload) => dispatch(random(payload));
randomBtn.addEventListener('click', () => {
  const payload = Math.floor(Math.random() * 10);
  randomDispatch(payload);
});

Для приложения с сотней action creators можно создать функцию bindActionCreators, которая будет соединять action creator и dispatch
const bindActionCreators = (creator, dispatch) => (...args) => {
  dispatch(creator(...args));
}

Её использование
const incDispatch = bindActionCreators(inc, dispatch);
incBtn.addEventListener('click', incDispatch);

Функция с дополнительным аргументом
const randomDispatch = bindActionCreators(random, dispatch);
randomBtn.addEventListener('click', () => {
  const payload = Math.floor(Math.random() * 10);
  randomDispatch(payload);
});

Функцию нет необходимости писать самостоятельно, она уже интегрирована в redux, достаточно её оттуда импортировать:
import { createStore, bindActionCreators } from 'redux';

Также можно создать одной строкой все dispatch из файла actions.js
Для этого вместо
import {inc, dec, random} from './actions';

пишем
import * as actions from './actions';

Затем
const {inc, dec, random} = bindActionCreators(actions, dispatch);

и теперь не названия action, а функции, в которых действие привязано к его результату

Редакс в реальной жизни


Особенности редакса 
1) всё состояние приложения хранится в единственном месте - store
2) приложение строится по однонаправленному потоку данных


3) Состояние нельзя изменять напрямую. 
То есть вот так: store.number += 1 — делать нельзя.
4) Чтобы изменить стор используем store.dispatch()
5) В store.dispatch() передаём action
action это объект, который может содержать любые поля, единственное обязательное поле — type
store.dispatch({
  type: 'INCREMENT',
  by: 1
})
6) все изменения обрабатываются функцией, которая называется «редьюсер» и которая должна быть чистой

Главные принципы редакса
  • Всё состояние приложения хранится в единственном месте сторе
  • Состояние нельзя изменять напрямую, нужно создавать экшены
  • Экшены обрабатываются чистой функцией редьюсером

Что делать с сайд эффектами?

Сайд-эффекты — это когда вы изменяете глобальную переменную, делаете запрос на сервер, пишете что-то в лог.

Проблема в том, что в редаксе сайд-эффектам нет места. Единственное место, где происходят изменения — это редьюсер, а он обязан быть чистой функцией.

Сайд эффекты создаём при помощи middleware, которые находятся между store.dispatch() и редьюсером


middleware может быть много, тогда они образуют цепочки



Пример middleware

const logMiddleware = store => next => action => { 
  console.log(action); 
  next(action); }
const store = createStore (reducer, applyMiddleware (logMiddleware)) 

middleware подключаются при создании store
createStore и applyMiddleware — это методы редакса.

import { createStore } from 'redux';

const reducer = (state = 0, action) => {

  switch (action.type) {
    case 'INC':
      return state + 1;
    default:
      return state;
  }
};

const store = createStore(reducer);

store.subscribe(() => {
console.log(store.getState())
});

store.dispatch({type: 'INC'});
store.dispatch({type: 'INC'});