Концепция Redux не так уж сложна, и в абстрактном смысле, если вы создали очень глубоко устроенное приложение React, в котором все ваше состояние хранится в ‹App/›, а затем бесконечно течет через вашу иерархию компонентов, оно работает аналогично это, но в гораздо более упрощенной архитектуре. Redux централизует ваше состояние в так называемом «хранилище», но вместо того, чтобы каскадировать компоненты вниз по уровням, вы можете внедрять состояние (и диспетчеризацию — свойства функции, влияющие на состояние) непосредственно в компоненты, которые в них нуждаются.

Это обеспечивает два основных преимущества. (1) Намного проще вносить изменения, например, изменить имя ключа для части состояния. Вам больше не нужно изменять его на каждом уровне архитектуры компонентов, только там, где он явно используется. (2) Это сделает ваше приложение быстрее, потому что вместо повторного рендеринга всей ветки компонентов, начиная с приложения (запуская повторный рендеринг реквизита), он будет повторно отображать только фактически измененный компонент.

Где Redux начинает казаться немного чужим, так это в реализации, особенно если речь идет о чем-то вроде Redux-Saga, где может быть значительный шаблонный код. И если вы работаете в очень простом приложении (например, в приложении списка дел, любезно предоставленном примером документации Redux), вы можете подумать, что сделать что-то простое в React — это много работы — и это так! Для небольших и неиерархических одностраничных приложений Redux может оказаться не тем инструментом, который вам нужно использовать. Но для более масштабного приложения с глубокими ветвями компонентов и/или несколькими маршрутизируемыми страницами в конце концов вы обнаружите, что после установки процесс управления вашим приложением значительно упрощается.

Итак, как же работает Redux? По сути, в редукционной машине есть три винтика: хранилище, в котором хранится состояние нашего приложения, действия, определяющие мутации состояния, и редуктор(ы) которые сопоставляют действия с новыми объектами состояния (обратите внимание, что мы по-прежнему никогда не будем напрямую изменять состояние).

Хорошо, это звучит достаточно просто, как мы это настроим? Итак, вот простой пошаговый процесс настройки вашей архитектуры Redux:

(1) Настройка магазина

Лично мне нравится хранить всю мою логику редукции в папке редуктов (а затем в папках действий и редукторов). Итак, в этой папке я создаю файл store.js. Там все, что нам нужно, это:

import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import rootReducer from './reducers';
const composedEnhancers = compose(applyMiddleware(thunk, logger));
const store = createStore(rootReducer, composedEnhancers);
export default store;

Это все для магазина! Здесь я применяю пару промежуточных программ, но если вы просто делаете простое приложение с редукцией (без преобразователя или регистратора), вам не нужно будет создавать какие-либо энхансеры.

Сверху вниз мы импортируем наши функции из Redux ( createStore , applyMiddleware , compose ), а также наш Reducer из нашего файла reducers (который мы создадим на четвертом шаге — не волнуйтесь! На этом этапе может быть целесообразно настроить пустую функцию rootReducer и экспортировать ее). Затем мы «составляем» наше промежуточное программное обеспечение, применяя два промежуточных программного обеспечения, которые мы будем использовать. Функция createStore принимает два аргумента: нашу функцию редуктора и составное ПО промежуточного слоя.

Наконец, мы экспортируем.

(2) Оберните наше приложение в Redux Provider

Помните, как я сказал, что Redux очень похож на передачу состояния от компонента более высокого порядка? Ну вот этот компонент более высокого порядка:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import store from './redux/store';
import { Provider } from 'react-redux';
ReactDOM.render(
 <Provider store={store}>
  <App />
 </Provider>, document.getElementById('root'));

Мы импортируем магазин из нашего файла store.js и импортируем класс Provider из Redux. Затем мы оборачиваем наше приложение ‹App/› в компонент Provider и передаем его в хранилище в качестве реквизита.

(3) Определите свои действия

Действия (и создатели действий) — это своего рода странная часть экосистемы Redux. Это объекты, содержащие всю информацию, которая потребуется нашему редюсеру для создания нового состояния. Вместе вы можете думать о них как о своих функциях-обработчиках (вроде). В действиях будет имя вашего обработчика, которое вы сохраните в переменной, например:

export const LOGIN = "LOGIN";

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

Итак, теперь у нас есть бесполезная переменная… как выглядит этот эффект? С создателем действий! Создатели действий — это просто функции, которые возвращают объекты, содержащие всю информацию, необходимую для изменения состояния. Например, иногда для переключения логических значений они могут включать только действие, но для других они будут передавать данные. Для создателя действия входа:

export function login(user, token) {
 return {
  type: LOGIN,
  user: user,
  token: token
 }
};

Создатель действия входа в систему отправит пользователя и данные токена доступа вместе с действием («типом» действия в создателе) редюсеру.

(4) Настройка нашего rootReducer и initialState

Хорошо, момент истины: редуктор! Вот где волшебство происходит в редуксе. Редьюсер немного похож на мини-сервер, он состоит из функции-переключателя, которая принимает тип действия и интерпретирует его как маршрут, чтобы направить его в правильный случай, который содержит логику для фактического возврата нового состояния.

import { LOGIN, LOGOUT } from './actions/auth-actions';
const initialState = {
 isLoggedIn: false,
 user: null,
 token: null,
 otherState: null
};
const rootReducer = (state = initialState, action) => {
 switch(action.type) {
  case LOGIN:
   return {
    ...state,
    isLoggedIn: true,
    token: action.token,
    user: action.user,
   };
  case LOGOUT:
   return {
    ...state,
    isLoggedIn: false,
    token: null,
    user: null
   };
  default:
   return {
    ...state
   }
  }
}
export default rootReducer;

Это, конечно, очень упрощенный редьюсер, который будет обрабатывать только вход и выход — в моем большом приложении и в вашем у него будет гораздо больше состояний для управления. Я добавил ключ «otherState», чтобы проиллюстрировать, как вы хотите распространять состояние.

Сначала мы импортируем наши действия (не создатели действий). Они будут интерпретироваться, чтобы определить, как мы маршрутизируем действия.

Мы определим наше initialState, как и в компоненте React, дадим всем вашим частям состояния ключ и начальное значение.

rootReducer (у вас также может быть несколько редюсеров, которые вы затем объедините с помощью метода Redux) будет содержать логику, связанную с каждым действием, и будет принимать два входа. Первый аргумент — это состояние, которое по умолчанию будет равно initialState. Второй — действие. Когда мы отправляем наши действия, они будут отправлены редюсеру в качестве аргументов.

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

В каждом случае мы вернем новый объект, который заменит объект состояния в нашем хранилище. Сначала мы хотим распространить ключи нашего объекта состояния с помощью …state. Любая часть состояния, которую мы изменим, перезапишет это состояние распространения, но мы не хотим вручную включать каждый ключ и значение для нашего объекта состояния… Затем мы перечисляем ключи и новые значения, которые мы хотим изменить в обновленном состоянии. .

Убедитесь, что вы также включили случай по умолчанию, который просто возвращает предыдущее состояние!

(5) Подключайте компоненты по мере необходимости

Какой бы компонент ни нуждался в доступе к хранилищу избыточности и/или actionCreators, вы импортируете вверху:

import { bindActionCreators } from 'redux';
import { login } from '../../redux/actions/auth-actions';
import { connect } from 'react-redux';

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

Нам также нужно будет сопоставить объект состояния, который мы получаем из хранилища, с реквизитами этого компонента, которые нам нужно будет определить в двух разных функциях:

const mapStateToProps = state => {
 return {
  user: state.user,
  token: state.token,
  isLoggedIn: state.isLoggedIn,
 }
};
const mapDispatchToProps = dispatch => bindActionCreators({
 login: login
}, dispatch);

Сопоставление состояния с реквизитами проще всего понять. Вы создаете функцию, которая принимает состояние (из хранилища) в качестве аргумента и определяете новый объект (и объекты, которые будут объединены с вашим объектом реквизита), где вы называете ключи, которые вы ожидаете в качестве реквизита, где значения ключи из объекта состояния вашего магазина.

Отображение диспетчеризации немного более абстрактно, так как для выполнения работы вы будете использовать часть интерфейса redux (dispatch и bindActionCreators). Вы создаете объект, который будет принимать функцию отправки (которая на самом деле отправляет действия редюсеру) и передавать ее в bindActionCreators, который сам принимает два аргумента: объект создателей действий, связанных с их именами свойств, и функцию отправки.

Эта часть может показаться немного абстрактной, потому что большая часть работы выполняется за кулисами в рамках того, что вы настроили в коде шаблонной пластины редукции, но вы быстро адаптируетесь!

Наконец, вам нужно подключить ваш компонент к этим двум функциям. И функция подключения делает именно это:

export default connect(mapStateToProps, mapDispatchToProps)(LoginPage);

Функция подключения принимает эти две функции. Порядок имеет значение! если вам не нужно передавать какое-либо состояние, вы сделаете: connect(null, mapDispatchToProps). Если вам не нужно передавать диспетчеризацию, то вы можете просто передать один аргумент, который будет иметь значение mapStateToProps. Функция, которую вы создаете с помощью connect(), будет немедленно вызвана и примет ваш Компонент в качестве аргумента — и все это будет экспортировано!

Фу ты настроился!

(6) А как насчет thunk?

Что делать, если вам нужно установить состояние из вызова API? Поскольку эти вызовы являются асинхронными, vanilla Redux не может ими управлять, поэтому вам нужно будет использовать промежуточное программное обеспечение. redux-thunk, redux-promise, redux-saga — у вас есть несколько вариантов, но для меня Thunk проще всего использовать. И для приложений малого или среднего размера это работает очень хорошо.

Преобразователь вернет новую функцию отправки, которая позволит асинхронные вызовы axios. Это так просто!

Один вызов API, который я буду использовать для загрузки плейлиста Mood. Вот функция thunk:

import { fetchMoodPending, fetchMoodSuccess, fetchMoodError } from './actions';
import axios from 'axios';
import apiPath from '../../App';
function fetchMood(moodId, token) {
 return dispatch => {
  dispatch(fetchMoodPending());
   axios.get(apiPath + moodId, {headers: {Authorization: "Bearer: " + token}})
   .then(res => {
    if (res.error) {
     throw(res.error)
    }
    dispatch(fetchMoodSuccess(res.data.name, res.data.songs));
    return res.data;
   })
   .catch(err => {
    dispatch(fetchMoodError(err));
   });
  }
}
export default fetchMood;

Итак, для вызовов API принято выполнять несколько действий, связанных с состоянием этого вызова. К ним относятся «выборка», указывающая на то, что вызов выполняется, «успех», указывающий на то, что вызов был выполнен успешно, или «ошибка»/«сбой», указывающая… на сбой. Это может быть полезно, если вы делаете большой вызов и хотите, чтобы какая-то анимация загрузки выполнялась, например, пока состояние «выборки» истинно, или запускать обработку ошибок внешнего интерфейса, если «ошибка» возвращается.

Мы объявляем функцию, которая будет принимать идентификатор настроения (и для целей аутентификации токен доступа). Все, что он делает, это возвращает новую анонимную функцию, которая принимает функцию отправки redux в качестве аргумента.

Первое, что мы делаем, это отправляем создателя действия выборки, который является функцией, вызываемой внутри функции отправки. Это обновит наше состояние, чтобы показать, что вызов API извлекается. Затем мы делаем вызов axios к нашему API, который является асинхронной функцией, и присоединяем заголовок Authorization к вызову axios, поскольку это частный маршрут на нашем бэкэнде.

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

И это все!

Идите вперед и обновите свое большое старое приложение!