Внедрение Redux в приложение, делает его центральной, корневой частью всего приложения. Причём не имеет значения, используем мы React или нет. Структура контейнера и принцип работы с ним останется неизменным в любой ситуации. Общая схема работы приложения становится такой:

  1. Возникает событие. Например, пользователь кликнул по кнопке.
  2. Обработчик события выполняет какую-то логику и в конце обновляет контейнер через store.dispatch.
  3. Контейнер по очереди вызывает все функции, добавленные через store.subscribe. Эти функции меняют представление на основе нового состояния внутри контейнера. И так по кругу: Событие -> Изменение состояния -> Отрисовка нового состояния.

Реализуем эту логику в связке с React. Для примера возьмём простой компонент счётчик с одной кнопкой, которая отображает текущее количество кликов. Связку с React сделаем в ручном режиме без использования готовой библиотеки. Тогда процесс работы не покажется магическим. Начнём с контейнера:

import { createStore } from 'redux';

const reducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    default:
      return state;
  }
};

const store = createStore(reducer);

Контейнер ничего не знает про существование DOM, его задача - хранить данные и модифицировать их. Эта мысль очень важна, её нужно прочувствовать. Воспринимайте контейнер как базу данных.

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

В будущих уроках мы рассмотрим ситуации, когда внутреннее управление состоянием всё ещё требуется, несмотря на использование Redux

import React from 'react';

export default class Increment extends React.Component {
  static defaultProps = {
    count: 0,
  };

  render() {
    const { count } = this.props;
    return (
      <div>
        <button>{count}</button>
      </div>
    )
  }
}

Компонент Increment работает с пропом count. Его имя выбрано произвольно, нам не нужно опираться на структуру контейнера.

Теперь добавим обработчики. Напомню, что каждый обработчик в конце своей работы должен обновить состояние контейнера. С технической точки зрения произойдёт вызов функции store.dispatch и нужного действия. Откуда нам их взять внутри компонента? Всё просто, мы их прокинем как свойства в наш компонент.

import React from 'react';

export default class Increment extends React.Component {
  handleClick = (e) => {
    const { dispatch, increment } = this.props;
    dispatch(increment());
  }

  render() {
    const { count } = this.props;
    return (
      <div>
        <button onClick={this.handleClick}>{count}</button>
      </div>
    )
  }
}

Остался последний шаг: нужно вызывать перерисовку компонента после изменения содержимого контейнера. В этом нам поможет функция store.subscribe:

import ReactDOM from 'react-dom';
import React from 'react';
import { createStore } from 'redux';

// Импортируем компонент
import Increment from './components/Increment';
// Импортируем редьюсеры
import reducers from './reducers';

// Создаём контейнер. редьюсеры описаны в отдельном файле
const store = createStore(reducers);

// Создаём действие и оборачиваем его в функцию
export const increment = () => ({
  type: 'INCREMENT',
  payload: {},
});

// Элемент для подключения React
const containerElement = document.getElementById('container');

// Подписываемся на изменения состояния внутри контейнера
// На каждое изменение отрисовываем наш компонент заново
store.subscribe(() => {
  const state = store.getState();
  ReactDOM.render(
    <Increment dispatch={store.dispatch} count={state} increment={increment} />,
    containerElement,
  );
});


// Первый раз нужно отрисовать руками
ReactDOM.render(
  <Increment dispatch={store.dispatch} increment={increment} />,
  containerElement,
);

Когда все необходимые объекты созданы, происходит первоначальная отрисовка компонента в DOM. В компонент передаются необходимые данные, в нашем случае функция store.dispatch и функция increment. Последняя создаёт действие при своём вызове. Дальше начинает работать последовательность шагов, описанная в начале урока:

  1. Пользователь нажимает на кнопку
  2. Срабатывает обработчик handleClick, который вызывает dispatch(increment()).
  3. Выполняется редьюсер и его ветка INCREMENT. Она увеличивает счётчик на единицу.
  4. Контейнер вызывает функции, добавленные через subscribe. В нашем случае это одна функция.
  5. Эта функция извлекает состояние из контейнера через функцию store.getState.
  6. Затем эта же функция перерисовывает компонент в DOM передавая ей новое состояние.

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

export const increment = (step = 1) => ({
  type: 'INCREMENT',
  payload: { step },
});

Такой инкремент позволяет менять шаг приращения. Внутри контейнера код поменяется на такой:

case 'INCREMENT':
  return state + action.payload.step;

Само состояние внутри контейнера может стать структурой, например объектом.

Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →