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

Глобальное состояние приложения — это часть состояния приложения, которое используется, учитывается и изменяется в разных частях, модулях, компонентах этого приложения.

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

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


Из чего состоит Flux-архитектура

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

Действия котика и его состояние составляют сущность его существа. Мы можем смоделировать такое поведение и в наших программах с помощью компонентов. Каждый компонент содержит необходимые данные и определяет методы их изменения. Такой компонентный подход может быть реализован в архитектуре MVC.

Когда объектов становится много, наступает хаос. В программе так тоже бывает. Когда компонентов в архитектуре MVC много, и каждый из них что-то сообщает остальным, а все в ответ на это что-то изменяют в своих состояниях и снова сообщают об этом другим компонентам — наступает хаос. А всё потому что у каждого компонента хранится часть состояния, которая важна не только ему, но и другим компонентам.

Во Flux-архитектуре такое глобальное состояние предложено вынести в хранилища, отделить его от компонентов. А изменение состояния производить через специальные ворота, называемые диспетчерами. В этом случае поток управления меняет направление: изменение состояния переходит от представлений к диспетчеру, а новое состояние предоставляется всем представлениям сразу.

Отрывки текста с выделением по умолчанию.

Во Flux-архитектуре состояние является общим для многих представлений.

Вот небольшая иллюстрация этой идеи. В хранилище можно сохранить список сообщений с признаками — название чата и флаг «прочтено».

  • Одно представление может выбирать из этого списка и агрегировать только названия чатов — это будет компонент Chats.
  • Второе представление может выбирать и подсчитывать из этого списка количество непрочитанных сообщений для каждого чата и изображать бейджик.
  • Третье представление может фильтровать сообщения одного из чатов и показывать их, и это будет Messages компонент.

При этом, если пользователь отметит, что сообщения в конкретном чате «прочитаны», соседние представления обновятся не потому что их уведомил компонент Messages, а потому что изменилось глобальное состояние приложения.

Что такое Redux?

Flux-архитектура — это идея. Идею можно реализовать разными способами, и один из них — Redux.

Redux — это реализация Flux на JavaScript. Эту реализацию удобно применять в веб-приложениях для управления общим глобальным состоянием. У неё есть специальная привязка для использования с React, о которой мы поговорим позднее.

Redux удобно использовать, когда

  • Требуется синхронизировать состояние между многими компонентами;
  • Постоянно приходится контролировать изменение состояния во времени — time travel debug;
  • Приходится перехватывать и вмешиваться в процесс изменения состояния используя middleware.

Использовать Redux можно и в vanila javascript приложениях. Для этого надо подключить библиотеку, например, из CDN.

Познакомимся поближе с составными частями Redux.


Анатомия Redux — action, store, reducer, dispatch.

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

Action

Как и в оригинальной идее Flux, у Redux есть понятие действия — Action. Action это обычный объект JavaScript. Этот объект попадается на глаза разработчику дважды. В первый раз, когда JavaScript реагирует на событие, а во второй раз, когда сведения из объекта встраиваются в хранилище.

В нашем приложении объект, описывающий действие, будет содержать дату, время и координаты мыши. Этот объект можно создать с помощью функции-фабрики.

const MOUSE_CLICK = "MOUSE_CLICK";

function createMouseClickAction(evt) {
  return {
    type: MOUSE_CLICK,
    payload: {
      time: new Date().toISOString(),
      clientX: evt.clientX,
      clientY: evt.clientY,
    }
  }
}

reducer

Написанная нами функция createMouseClickAction возвращает объект с двумя полями — type и payload. Мы можем использовать полученный объект, чтобы обновить наш список. Реализуем это обновление в специальной функции. Допустим, что в параметрах она будет получать текущий список и наш объект и добавлять payload в конец списка.

function reducer(state=[], action){ 
  if(action.type === MOUSE_CLICK){
    const newState = [...state];
    newState.push(action.payload);
    return newState;
  }
  return state;
}

Мы назвали функцию обновления состояния reducer, но это просто дань традиции. Всё же функция имеет определенные особенности. Она

  • Не обновляет полученный массив, а копирует его;
  • Проверяет тип аргумента action;
  • Возвращает исходное состояние, если action не понятен.

store

В составе библиотеки redux.js есть функция создания хранилища createStore. Используем её для создания хранилища и подключим к нему несколько представлений, которые создадим такой функцией.

function createView(element){
  store.subscribe(()=>{
    element.innerHTML = '';
    const state = store.getState();
    state.forEach((e)=>{
      const li = document.createElement('li');
      li.innerText = `time: ${e.time}, x: ${e.clientX}, y: ${e.clientY}`;
      element.appendChild(li);
    });
  });
}

dispatch

Закончим приложение определением обработчика click. В нём мы будем использовать ещё одну составляющую часть Redux — dispatch — именно этот метод позволяет отправить действие диспетчеру и изменить состояние приложения.

document.addEventListener('click',(evt) => {
  store.dispatch(createMouseClickAction(evt));
})

На этом приложение закончено. Работающий пример и полный код примера можно найти на JSFiddle.

Всё то же самое, но в связке с React

Redux создавали для использования вместе с React, а сама связка помещена создателями в пакет react-redux. В нём содержатся сервисные функции для удобства использования store.subscribe, store.dispatch. Доступны два стиля их использования:

  • Функция connect создает обертку вокруг вашего компонента и подключает его к хранилищу.
  • Хуки useDispatch, useSelector делают ту же работу, но для функциональных компонентов.

Реализуем в React ту же функциональность, что и в предыдущем разделе. Дополнительно нам потребуется библиотека react-redux.

Функции createMouseClickAction и reducer остаются без изменений.

Вместо createView мы создадим компонент View:

const itemToString = (item)=>`time:${item.time}, x: ${item.clientX}, y: ${item.clientY}`;

const View=()=>{
  const state = ReactRedux.useSelector((state)=>state);
  return (<ul>
    {state.map((item)=>(<li key={item.time}>{itemToString(item)}</li>))}
  </ul>)
}

В этом коде сравните строки

const state = ReactRedux.useSelector((state)=>state);
const state = store.getState(); 

из предыдущего примера.

Вместо прямого подключения обработчика событий используем useEffect для своевременного снятия обработчика.

const App = ()=>{
  const dispatch = ReactRedux.useDispatch();
  React.useEffect(()=>{
    const handleClick = (evt)=>{
      dispatch(createMouseClickAction(evt));
    }   
    document.addEventListener('click', handleClick);
    return ()=>{
      document.removeEventListener('click', handleClick);
    };
  });
  return (<div className="container">
    <View/>
    <View/>
  </div>);
}

Обратите внимание на способ получения функции dispatch.

Функции useSelector и useDispatch могут выполнять свою роль, только если мы правильно присоединим React-приложение к хранилищу store нашего Redux.

Вы помните, что в предыдущем приложении мы создавали хранилище так:

const store = Redux.createStore(reducer).

В этот раз ничего не поменялось. А вот созданное хранилище мы подключим к приложению React с помощью ReactRedux.Provider.

ReactDOM.render(
  (<ReactRedux.Provider store={store}>
    <App />
  </ReactRedux.Provider>),
  document.querySelector("#root")
 )

На этом всё — мы сделали такое же приложение, только на React. Собранное работающее приложение можно найти на JSFiddle.

В качестве самостоятельной работы попробуйте модифицировать пример так, чтобы в левой части страницы перечислялись записи от нажатии мыши в левой части экрана, а в правом view — правые клики.


Альтернативы Redux

Redux — далеко не единственная возможность управления глобальным состоянием.

В банковском приложении один из атрибутов глобального состояния — баланс клиентского счета. Очевидно, что источник этого значения не может находиться на стороне браузера, потому что правильное значение баланса — ответственность банковского сервера.

Правильно ли использовать в таком случае использовать Redux для хранения остатков по счетам? Похоже, что нет.

Redux — не лучшее решение для ситуации, когда глобальное состояние — более глобальное, чем окно браузера.

Поэтому следует знать о альтернативных возможностях управления состоянием

  • Mobx — скрывает функциональное программирование для управления состоянием
  • SWR — реализует стратегию кеширования по HTTP RFC 5861
  • GraphQL — язык и его реализация для запросов данных с сервера
  • И некоторые другие варианты.

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

На профессии «React-разработчик» научим и не такому

А чему научим — легко можем рассказать.

Расскажите