Redux на практике: осваиваем действия в приложении

Хватит ждать, пора действовать! Изучаем действия в приложении на Redux, сразу применяя теоретический материал на практике.

Посмотрите знакомство с Redux и первое приложение.

Дизайн макета обновлён. При нажатии на любую из кнопок для обновления состояния пользователь чувствует взаимодействие с приложением:

Redux на практике: осваиваем действия в приложении
Пример работы в GIF

Что есть действие в Redux?

При посещении банка кассир узнаёт о вашем намерении снять деньги. WITHDRAWAL_MONEY – оно же является действием. Чтобы получить деньги, нужно взаимодействовать с кассиром.

В отличие от setState(), единственный способ обновить состояние в Redux – сообщить об этом reducer:

{
  type: "withdraw_money"
}

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

{
  type: "withdraw_money",
  amount: "$4000"
}

Представим, что номер банковского счета не нужно сообщать, и тогда этой информации становится достаточно.

Вы не можете серьезно модифицировать функцию type. Дополнительная информация задаётся в payload:

{
  type: " ",
  payload: {}
}

Обработка данных в редукторах

Мы говорили, что reducer принимает два значения – state и action. Простой reducer выглядит так:

function reducer(state, action) {
 //вернуть новое состояние
}

Чтобы reducer обработал функцию, нужно задать ему инструкцию, используя switch:

function reducer (state, action) {
  switch (action.type) {
    case "withdraw_money":
        //сделать что-нибудь
    break;
    case "deposit-money":
        //сделать что-нибудь
    break;
    default:
    return state;
  }
}

В данном примере switch запустит механизм исполнения действия в приложении. Допустим, у вас было 2 кнопки:

{
  isOpen: true,
  isClicked: false,
}

При нажатии на первую кнопку приложение переходит в состояние isOpen:

this.setState({isOpen: !this.state.isOpen})

А при нажатии на вторую обновляется поле isClicked:

this.setState({isClicked: !this.state.isClicked})

В Redux не получится использовать setState(): сначала нужно сообщить о действии.

№1

{
  type: "is_open"
}

№2

{
  type: "is_clicked"
}

В Redux-приложении всё проходит через редуктор. Поэтому мы ввели поле action.type, чтобы reducer определял нужное действие:

function reducer (state, action) {
  switch (action.type) {
    case "is_open":
        return; //вернуть новое состояние
    case "is_clicked":
        return; //вернуть новое состояние
    default:
    return state;
  }
}

Redux на практике: осваиваем действия в приложении

Важно обрабатывать каждый тип по отдельности. Для этого и нужен switch.

Обработка действия в приложении

Для обновления состояния приложений нужно сообщать action, неважно каким способом. При каждом нажатии кнопки нам нужно отправить действие:

Redux на практике: осваиваем действия в приложении

Посмотрите, как это будет работать с каждой из кнопок.

React

{
  type: "SET_TECHNOLOGY",
  text: "React"
}

React-redux

{
  type: "SET_TECHNOLOGY",
  text: "React-redux"
}

Elm

{
  type: "SET_TECHNOLOGY",
  text: "Elm"
}

Все действия имеют одно и то же поле type. Как в банке: пополнение счёта производилось бы на разные суммы. Общая функция DEPOSIT_MONEY, разное количество amount.

Дублирование кода

Давайте уменьшим количество повторяющегося кода. Для этого в Redux есть Action Creators. Это функции, которые создают объекты-действия. Например, в нашем случае можно создать функцию setTechnology, принимающую аргумент "текст":

function setTechnology(text) {
  return {
    type: "SET_TECHNOLOGY",
    text: text
  }
}

Объединяем пройденное

Итак, когда вы приходите в банк, кассир сидит за столом и ожидает клиентов, а сейф с деньгами ждёт своего часа в соседней комнате. В Redux каждый участник цепочки (reducer, action, store) тоже находится на своём месте – в отдельной папке. Обычно создают 3 папки:

Redux

В каждой из папок создаем файл index.js, что позволит начать работу каждой функции. Теперь будем работать с нашим приложением из статьи.

store/index.js

import { createStore } from "redux";
import reducer from "../reducers";

const initialState = { tech: "React " };
export const store = createStore(reducer, initialState);

Это работает так же, как и ранее. Отличие в том, что хранилище создаётся в отдельном index.js файле. Если нам нужен store, будем писать:

App.js

import React, { Component } from "react";
import HelloWorld from "./HelloWorld";
import ButtonGroup from "./ButtonGroup";
import { store } from "./store";

class App extends Component {
 render() {
  return [
         <HelloWorld key={1} tech={store.getState().tech} />,
         <ButtonGroup key={2} technologies={["React", "Elm", "React-redux"]} />
         ];
 }
}

export default App;

В чем отличия? В четвертой строке хранилище импортируется из собственной «комнаты». Кроме того, здесь есть компонент, который отвечает за работу кнопок.

Redux

Следующая особенность в том, что приложение возвращает массив. Это стало доступно в React 16.

react

Так это работает для компонента App.js.

Реализация ButtonGroup проще:

import React from "react";

const ButtonGroup = ({ technologies }) => (
  <div>
    {technologies.map((tech, i) => (
      <button
        data-tech={tech}
        key={`btn-${i}`}
        className="hello-btn"
      >
        {tech}
      </button>
    ))}
  </div>
);

export default ButtonGroup;

ButtonGroup не имеет состояния. Он просто принимает массив названий технологий и с помощью map генерирует button для каждого элемента. В данном примере передаётся массив ["React", "Elm", "React-redux"]. У кнопок есть несколько атрибутов:

  • className для стиля;
  • key, который вы будете постоянно забывать;
  • атрибут данных data-btn для более простого извлечения некоторых значений из элементов.

Сгенерированная кнопка выглядит так:

<button 
  data-tech="React" 
  key="btn-1" 
  className="hello-btn"> React </button>

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

Так происходит потому, что мы не настроили обработчик кликов. Внутри функции render определим действие для onClick:

<div>
    {technologies.map((tech, i) => (
      <button
        data-tech={tech}
        key={`btn-${i}`}
        className="hello-btn"
        onClick={dispatchBtnAction}
      >
        {tech}
      </button>
    ))}
  </div>

Хорошо. Теперь определим dispatchBtnAction.

Не забудьте, что основная цель обработчика – послать действие на обработку. Если нажать на кнопку "React", произойдет следующее действие:

{
    type: "SET_TECHNOLOGY",
    tech: "React"
}

А если нажать на "React-Redux":

{
     type: "SET_TECHNOLOGY",
     tech: "React-redux"
}

Вот так выглядит функция dispatchBtnAction:

function dispatchBtnAction(e) {
  const tech = e.target.dataset.tech;
  store.dispatch(setTechnology(tech));
}

Имеет ли смысл код выше?

e.target.dataset.tech получит атрибут данных с кнопки data-tech. Таким образом, константа tech будет хранить текст кнопки. store.dispatch() – способ
отправки действия в приложении на Redux, а setTechnology() – генератор действий, написанный нами ранее. То есть store.dispatch принимает, а setTechnology – создаёт.

function setTechnology (text) {
  return {
     type: "SET_TECHNOLOGY",
     text: text
   }
}

Код на изображении ниже должен помочь разобраться в происходящем:

Что происходит после отправки действия?

Для начала несложный вопрос. Что происходит после нажатия кнопки (и отправки действия)? Какой участник появляется в цепочке Redux?

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

reducers/index.js

export default (state, action) => {
  console.log(action);
  return state;
};

Reducer возвращает начальное состояние. Через console.log() можно посмотреть, что происходит при нажатии на кнопку.

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

Но есть один нюанс. При запуске приложения учитывается лишнее действие:

{type: "@@redux/INITu.r.5.b.c"}

Это обычная инициализация, не оказывающая негативного влияния на работу программы.

Создание счётчика редуктора

До текущего момента мы писали приложение, которое ничего не делает. Как кассир, который не умеет делать WITHDRAW_MONEY. Что мы хотим от редуктора? При создании хранилища мы передали initialState в createStore.

const initialState = { tech: "React" };
export const store = createStore(reducer, initialState);

Когда пользователь нажимает на любую из кнопок, редуктор должен модифицировать состояние:

{
  type: "SET_TECHNOLOGY",
  text: "React-Redux"
}

Цель последнего действия – обновление состояния. Будем использовать switch для обработки разных действий:

export default (state, action) => {
  switch (action.type) {
    case "SET_TECHNOLOGY":
        //сделать что-нибудь
   default:
   return state;
  }
};

Но теперь в фокусе switch будет action.type. Почему? В качестве операций, которые может выполнить кассир – снять наличные, внести на счет, etc. Наш редуктор выполняет действие SET_TECHNOLOGY, но позднее могут быть и другие. Этот единственный case будет отвечать за переключение технологии на любое значение.

Не забывайте, что во всех прочих случаях не стоит выполнять никаких действий. Нужно просто вернуть текущее состояние state.

Кассир (редуктор) понимает, что вы хотите сделать, но не возвращает никакого ответа. Исправим это:

export default (state, action) => {
  switch (action.type) {
    case "SET_TECHNOLOGY":
      return {
        ...state,
        tech: action.tech
      };

    default:
      return state;
  }
};

Что только что произошло – объясняем ниже.

Никогда не меняйте состояние внутри редукторов

Первое, что хочется сделать – изменить state и вернуть его (код ниже). Если вы уже знакомы с хорошим стилем в React, то знаете, что это ошибка:

export default (state, action) => {
  switch (action.type) {
    case "SET_TECHNOLOGY":
      state.tech = action.tech; 
      return state;

    default:
      return state;
  }
};

Редуктор и вернул вот это:

return {
        ...state,
        tech: action.tech
};

Вместо изменения состояния мы возвращаем новый объект. Он имеет все свойства предыдущего, но находится в другом пространстве.

Кроме того, редукторы должны быть чистыми функциями без вызовов API, обновления значений. Теперь воображаемый кассир выдаёт нам деньги. Попробуем нажимать на кнопки. Работает? Нет! По крайней мере, текст не обновляется. В чём дело?

Обновление хранилища

После снятия денег обычно приходит сообщение о совершении операции. В Redux тоже следует настроить получение уведомлений об успешном обновлении состояния.

В каждом хранилище есть метод store.subscribe(). Он вызывается всякий раз, когда изменяется состояние.

Итак, нам нужно обновить элемент на странице. Для этого используем render.

Посмотрим, как это работает в index.js:

ReactDOM.render(<App />, document.getElementById("root")

Приложение берёт компонент <App /> и отображает его в DOM.

const render = function() {
  ReactDOM.render(<App />, document.getElementById("root")
}

Используя принципы ES6, функцию можно упростить.

const render = () => ReactDOM.render(<App />, document.getElementById("root"));

render();

После каждого успешного обновления <App /> будет повторно отображаться с новыми значениями состояния:

class App extends Component {
  render() {
    return [
      <HelloWorld key={1} tech={store.getState().tech} />,
      <ButtonGroup key={2} technologies={["React", "Elm", "React-redux"]} />
    ];
  }
}

Работает!

Заключение

Что нужно было усвоить в этой главе:

  • В отличие от setState() в React, единственный способ обновления состояния приложения Redux – отправка действия.
  • Действие описывается с помощью JavaScript-объекта с информацией о типе.
  • В приложении Redux каждое действие проходит через редуктор.
  • Используя оператор switch, можно обрабатывать разные типы действий в редукторе.
  • Action Creators – это функции, возвращающие объекты действий.
  • Основные участники цепочки Redux живут в своих папках.
  • Вы не должны изменять состояние. Нужно возвращать новую копию состояния.
  • Чтобы подписаться на сохранение обновлений, используйте метод store.subscribe().

Больше информации вы найдете здесь.

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик С#
от 200000 RUB до 400000 RUB
Java Team Lead
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ