Введение в Redux

Последнее обновление: 03.04.2022

Еще одну форму построения архитектуры приложения на React представляет Redux. Redux представляет собой контейнер для управления состоянием приложения и во многом напоминает Flux. Redux не привязан непосредственно к React.js и может также использоваться с другими js-библиотеками и фреймворками.

Ключевые моменты Redux:

  • Хранилище (store): хранит состояние приложения

  • Действия (actions): некоторый набор информации, который исходит от приложения к хранилищу и который указывает, что именно нужно сделать. Для передачи этой информации у хранилища вызывается метод dispatch().

  • Создатели действий (action creators): функции, которые создают действия

  • Reducer : функция (или несколько функций), которая получает действие и в соответствии с этим действием изменяет состояние хранилища

Общую схему взаимодействия элементов архитектуры Redux можно выразить следующим образом:

Redux в React.JS

Из View (то есть из компонентов React) мы посылаем действие, это действие получает функция reducer, которая в соответствии с действием обновляет состояние хранилища. Затем компоненты React применяют обновленное состояние из хранилища.

Рассмотрим, как все это будет работать в приложении на React.

Итак, создадим для проекта новый каталог и определим в нем файл package.json:

{
  "name": "reduxapp",
  "description": "A React.js project using Redux",
  "version": "1.0.0",
  "author": "metanit.com",
  "scripts": {
    "dev": "webpack serve",
    "build": "webpack"
  },
  "dependencies": {
    "react": "18.0.0",
    "react-dom": "18.0.0",
    "react-redux": "7.2.8",
    "redux": "4.1.2",
     "immutable": "4.0.0"
  },
  "devDependencies": {
    "@babel/cli": "7.17.0",
    "@babel/core": "7.17.0",
    "@babel/preset-react": "7.16.0",
    "babel-loader": "8.2.0",
    "webpack": "5.70.0",
    "webpack-cli": "4.10.0",
    "webpack-dev-server": "4.7.0"
  }
}

Для работы с Redux в React нам понадобятся зависимости "redux" и "react-redux". Кроме того, для работы с данными будем использовать тип Immutable.Map, поэтому также добавляем зависимость "immutable". А в узле "devDependencies" определены зависимости babel и webpack, которые потребуются для компиляции и сборки кода приложения.

Также добавим в каталог проекта новый файл webpack.config.js:

const path = require("path");
  
module.exports = {
    mode: "development",
    entry: "./app/app.jsx", // входная точка - исходный файл
    output:{
        path: path.resolve(__dirname, "./public"),     // путь к каталогу выходных файлов - папка public
        publicPath: "/public/",
        filename: "bundle.js"       // название создаваемого файла
    },
    devServer: {
     historyApiFallback: true,
     static: {
      directory: path.join(__dirname, "/"),
    },
     port: 8081,
     open: true
   },
    module:{
        rules:[   //загрузчик для jsx
            {
                test: /\.jsx?$/, // определяем тип файлов
                exclude: /(node_modules)/,  // исключаем из обработки папку node_modules
                loader: "babel-loader",   // определяем загрузчик
                options:{
                    presets:[ "@babel/preset-react"]    // используемые плагины
                }
            }
        ]
    }
}

В данном случае мы определяем, что исходный файл приложения будет находится по пути "app/app.jsx", а компилируемый файл будет находиться по пути "public/bundle.js".

И также добавим в каталог проекта веб-страницу index.html, на которой будет подключаться скомпилированный файл:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Hello Redux</title>
</head>
<body>
  <div id="app"></div>
  <script src="public/bundle.js"></script>
</body>
</html>

Определение действий

Теперь определим сам код приложения. Для этого определим в проекте новую папку app. В эту папку вначале добавим новый файл actions.jsx, который будет представлять действия:

const addPhone = function (phone) {
  return {
    type: "ADD_PHONE",
    phone
  }
};
const deletePhone = function (phone) {
  return {
    type: "DELETE_PHONE",
    phone
  }
};

module.exports = {addPhone, deletePhone};

Здесь определены две функции, которые создают действия: для добавления и удаления объекта. Каждое действие имеет свойство type, которое описывает действие, и свойство phone - непосредственно данные, которые передаются вместе с действием.

Reducer

Затем создадим в папке app новый файл reducer.jsx:

const Map = require("immutable").Map;

const reducer = function(state = Map(), action) {
  switch (action.type) {
    case "SET_STATE":
		return state.merge(action.state);
    case "ADD_PHONE":
		return state.update("phones", (phones) => [...phones, action.phone]);
    case "DELETE_PHONE":
		return state.update("phones",
			(phones) => phones.filter(
				(item) => item !== action.phone
			)
		);
  }
  return state;
}
module.exports = reducer;

Здесь описана функция reducer, которая получает действия и изменяет состояние хранилища.

Состояние хранилища будет представлять тип Immutable.Map, который представляет собой словарь, хранящий пары ключ-значение. В качестве ключей здесь используются названия свойств объекта.

В функции reducer при определении параметра присваиваем состоянию начальное значение - пустой словарь:

function(state = Map(), action)

Первый параметр - это собственно состояние хранилища. Второй параметр - action - передает действие. Так, как наши действия имеют свойство type, то мы можем получить это свойство и в зависимости от его значения тем или иным образом обновить состояние. Для обновления состояния применяются методы класса Immutable.Map. Каждый такой метод возвращает новый объект Immutable.Map.

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

return state.update("phones", (phones) => [...phones, action.phone]);

А для удаления мы просто возвращаем все те элементы, которые не равны удаляемому объекту. Для этого применяется функция phones.filter, которая выполняет фильтрацию:

return state.update("phones",
			(phones) => phones.filter(
				(item) => item !== action.phone
			)
		);

Кроме того, здесь применяется действие с типом "SET_STATE", которое просто возвращает начальное состояние хранилища:

return state.merge(action.state);

И после каждого обновления состояния нам надо возвратить обновленное состояние. Таким образом произойдет обновление хранилища.

Определение представления

Далее добавим в папку app файл appview.jsx, который будет определять всю визуальную часть:

const React = require("react");
const connect = require("react-redux").connect;
const actions = require("./actions.jsx");
 
class PhoneForm extends React.Component {
  constructor(props) {
    super(props);
    this.phoneInput = React.createRef();
  }
  onClick() {
    if (this.phoneInput.current.value !== "") {

      const itemText = this.phoneInput.current.value;
      this.phoneInput.current.value ="";
      return this.props.addPhone(itemText);
    }
  }
  render() {
    return <div>
            <input ref={this.phoneInput} />
            <button onClick = {this.onClick.bind(this)}>Добавить</button>
        </div>
  }
};
 
class PhoneItem extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
     
    return <div>
                <p>
                    <b>{this.props.text}</b><br />
                    <button onClick={() => this.props.deletePhone(this.props.text)}>Удалить</button> 
                </p>
            </div>
  }
};
 
class PhonesList extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return <div>
        {this.props.phones.map(item =>
          <PhoneItem key={item}
                    text={item}
                    deletePhone={this.props.deletePhone}/>
        )}
      </div>
  }
};
 
class AppView extends React.Component {
 
    render() {
        return <div>
            <PhoneForm addPhone={this.props.addPhone}/>
            <PhonesList {...this.props} />
    </div>
  }
};
 
function mapStateToProps(state) {
  return {
    phones: state.get("phones")
  };
}
 
module.exports = connect(mapStateToProps, actions)(AppView);

Собственно визуальная часть состоит из трех компонентов. Компонент PhoneForm используется для добавления нового объекта. PhoneItem представляет отдельный элемент в списке, а компонент PhonesList содержит список объектов из массива phones.

Корневым компонентом является AppView, в который помещаются все остальные компоненты. Вобщем здесь идет в основном логика передачи данных между компонентами через объект props. В частности, чтобы передать все данные из props компонента AppView в компонент PhonesList, используется выражение {...this.props}.

Но большое значение имеет то, что идет после определения класса AppView, в частности, выражение:

connect(mapStateToProps, actions)(AppView)

Функция connect из пакета "react-redux" позволяет связать хранилище и компонент (в данном случае AppView). Благодаря этому все данные из хранилища будут передавать в компонент через объект props. Дополнительно мы можем установить ряд настроек. Так, первая функция mapStateToProps(), которая передается в connect, позволяет установить сопоставление между объектами из состояния хранилища с объектам в props у компонента AppView. В данном случае мы просто устанавливаем, что значение this.props.phones в компоненте AppView будет передавать значение из объекта phones из хранилища:

function mapStateToProps(state) {
  return {
    phones: state.get("phones")
  };
}

Второй параметр в функции connect представляет набор действий, которые вызываются в компоненте AppView или в его дочерних компонентах. И опять же эти действия после этого мы сможем получить в компоненте AppView через значения this.props.addPhone и this.props.deletePhone.

Действие this.props.addPhone передается в компонент PhoneForm и в нем уже вызывается по клику на кнопку. А действие this.props.deletePhone передается в компонент PhonesList, а через него далее в PhoneItem и там также вызывается по нажатию на кнопку "Удалить".

Объединение всех компонентов приложения

Теперь определим в папке app основной файл app.jsx:

const React = require("react");
const ReactDOM = require("react-dom/client");
const redux = require("redux");
const Provider = require("react-redux").Provider;
const reducer = require("./reducer.jsx");
const AppView = require("./appview.jsx");

const store = redux.createStore(reducer);

store.dispatch({
  type: "SET_STATE",
  state: {
    phones: [ "Xiaomi Mi 10", "Samsung Galaxy Note20" ]
  }
});

ReactDOM.createRoot(
    document.getElementById("app")
)
.render(
  <Provider store={store}>
    <AppView />
  </Provider>
);

Вначале здесь собственно создается хранилище:

const store = redux.createStore(reducer);

В метод redux.createStore() следует передать функцию reducer, которая используется для обновления хранилища.

Используя метод store.dispatch(), можно выполнить какое-либо действие. В частности, здесь выполняется действие с типом "SET_STATE", которое устанавливает начальные данные для состояния хранилища.

Чтобы связать хранилище и компонент, применяется провайдер - класс Provider из пакета "react-redux". У провайдера устанавливается объект хранилища через свойство store: <Provider store={store}>. Поэтому именно это хранилище и будет использоваться для поставки данных в AppView через выше рассмотренную функцию connect.

Таким образом, весь проект будет выглядеть следующим образом:

Применение Redux в React

Затем перейдем в командной строке/терминале к каталогу проекта и для установки пакетов выполним команду:

npm install

И в конце запустим проект на выполнение командой npm run dev:

Проект React с Redux
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850