[Hexlet] JS: Redux (React)

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

Для решения, в том числе этих проблем, разработчики фейсбука придумали архитектуру Flux

Flux архитектура вводит ряд новых понятий, таких как:

  • Stores - хранилища, место в которое загружаются данные и в котором они обновляются. Хотя во Flux хранилища мутабельные, взаимодействовать с внешним миром внутри них нельзя. Никаких AJAX запросов, взаимодействия с DOM и подобных вещей. Менять данные напрямую тоже не получится, только посредством действий. Как видите, во Flux архитектуре менеджмент состояния приложения был вынесен наружу.
  • Actions - действия с помощью Dispatcher передаются в хранилища, которые на основе типа действия и данных пришедших с ним, сами себя обновляют.
  • Dispatcher - раскидывает действия по хранилищам.

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

С тех пор утекло много воды и мир шагнул вперед. В 2015 году, Dan Abramov, создал библиотеку под названием Redux заимствовав идеи из Flux и функционального языка Elm.

Сам по себе Redux, очень простая библиотека, предназначенная исключительно для менеджемента состояния. Она хоть и была разработана для использования в реакте, от него не зависит и может использоваться с чем угодно. Для ее связи с реактом понадобится библиотека react-redux , с помощью которой производится необходимая интеграция. В итоге получается структура крайне напоминающая Flux архитектуру, но со значительными упрощениями и улучшениями.

Кроме тех преимуществ, которые дает Flux, Redux привносит еще кое что:

  • Time traveling. возможность путешествовать по изменению состояния назад и вперед. Очень полезно при отладке, всегда можно отмотать (это не фигуральное выражение, а действительность) назад.
  • Удобная отладка и визуализация. Посредством Middlewares, Redux расширяется инструментарием, который предоставляет крайне удобные средства для отладки и визуализации происходящих внутри процессов. С ними мы познакомимся в ближайшее время чтобы сразу начать использовать всю мощь Redux.
  • Благодаря стандартизации работы с состоянием, которую привнес Redux, стало возможным автоматизировать практически все аспекты работы в React. Работа с формами, роутинг, асинхронность, история и многое другое.

В этом курсе, мы пройдем по основным возможностям редакса и интегрируем в создаваемое приложение, по ходу курса, множество разных библиотек. Мы не сможем копнуть в них глубоко, так как объем документации каждой библиотеки тянет на целую книгу, но разберем базовые варианты использования.

Основные темы:

  • Redux
  • Middlewares
  • Containers
  • Actions (Async)
  • Redux & React

Библиотеки:

  • reselect
  • redux-actions
  • redux-forms
  • redux-thunk

По ходу курса мы создадим приложение для работы с задачами и в конце интегрируем его с бекендом.

Redux - Predictable state container for JavaScript apps

Перед тем как начать, скажу сразу, что сам Redux прост до безобразия. Первые несколько уроков мы набъем руку по работе с ним, а уже после начнем интегрироваться с React, так как сама интеграция значительно сложнее.

import { createStore } from 'redux';

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

const store = createStore(reducer);
  1. Импортируется функция createStore , которая создает контейнер.
  2. Далее определяется, так называемый reducer . Функция, которая принимает на вход state и action . На выходе из функции возвращается новый state . Именно из-за сходства работы этой функции с тем как работает reduce , она имеет название reducer .
  3. Редьюсер передается в функцию createStore и на выходе мы имеем готовый к использованию контейнер.

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

// Функция `subscribe` является частью реализации паттерна Observer.
// Каждый ее вызов, добавляет в список наблюдателей новую функцию.
// Затем, как только меняются данные в хранилище, вызываются, по очереди, все наблюдатели.
store.subscribe(() =>
  console.log(store.getState()),
);
const incrementAction = { type: 'INCREMENT' };
store.dispatch(incrementAction);
// => 1

store.dispatch(incrementAction);
// => 2

const decrementAction = { type: 'DECREMENT' };
store.dispatch(decrementAction);
// => 1
  1. Единственный способ произвести изменения состояния в хранилище, передать Action в функцию dispatch . Action это обычный js объект, в котором присутствует минимум одно свойство - type . Никаких ограничений на содержимое этого свойства не накладывается, главное чтобы внутри контейнера был подходящий ему обработчик. Обратите внимание на то, что если был передан Action который не описан в Switch, reducer вернет само состояние. Отсюда, кстати, следует, что состояние должно быть инициализировано в определении функции.

Посмотрим еще один пример уже включающий в себя составные данные:

const reducer = (state = [], action) => { // инициализация состояния
  switch (action.type) {
    case 'USER_ADD': {
      const user = action.payload.user; // данные
      return [...state, user]; // Immutability
    }
    case 'USER_REMOVE': {
      const id = action.payload.id; // данные
      return state.filter(u => u.id !== id); // Immutability
    }
    default:
      return state;
  }
};

const user = /* ... */;
const addUser = { type: 'USER_ADD', payload: { user } };
store.dispatch(addUser);

Несмотря на то что ключ payload не обязательный и можно все данные складывать прямо в сам Action, я крайне рекомендую так не делать. Мешать в одном объекте статически заданные ключи с динамическими плохая идея. Кроме того в будущем, мы будем использовать библиотеки, которые требуют именно такого способа работы.

Three Principles

Подведем итог. Что главное в redux:

  • Single source of truth - используя редакс, мы работаем только с одним контейнером на приложение. Это одно из ключевых отличий от Flux архитектуры. Все состояние в одном месте.
  • State is read-only - Данные меняются только косвенно и используя функциональный стиль.
  • Changes are made with pure functions - внутри хранилища можно использовать только чистые функции. Тут правила даже строже чем во Flux, так как не позволяется использовать даже Date.now() и ему подобные функции. Которые хотя и не обладают побочными эффектами, но все же являются недетерминированными. Все подобные вызовы должны делаться до вызова dispatch (подробнее об этом далее).

Начальное состояние

Я говорил про то что начальное состояние задается в определении редьюсера:

const reducer = (state = 0, action) { /* ... */ }

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

const store = createStore(reducer, initState);
// @@redux/INIT

Redux посылает специальный Action, который нельзя перехватывать. Если редьюсер реализован правильно и содержит default секцию в switch , то контейнер заполнится данными из initState .

JS: Redux (React) Middlewares

Middlewares относятся к продвинутым техникам использования Redux. В данном уроке мы посмотрим на них не с точки зрения написания, а исключительно с точки зрения использования. Нам они потребуются для подключения различных библиотек буквально с первого момента использования совместно с React.

Если вы знакомы с мидллварами в Express.js, то они не станут для вас сюрпризом. Общий принцип работы таков. Мидлвары встраиваются в хранилище при его создании. Затем, во время диспатчинга, данные проходят через них и только затем попадают в редьюсер.

Такая организация библиотеки, позволяет ее крайне легко расширять новой функциональностью, по принципу паттерна Decorator.

Типичные примеры использования включают:

  • Логирование
  • Оповещение об ошибках
  • Работа с асинхронным API
  • Роутинг

Посмотрим как их подключить:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

const store = createStore(
  reducer,
  /* preloadedState, */
  applyMiddleware(thunk)
)

thunk это мидлвара, но перед тем как передать ее в функцию createStore , нужно применить к ней функцию applyMiddleware . Также обратите внимание на то, что мидлвару мы передаем вторым параметром, хотя в предыдущем уроке вторым параметром шел initState . Объясняется это просто, функция createStore проверяет тип второго параметра и в зависимости от этого понимает что перед ней. В общем случае она принимает три параметра: редьюсер, начальный стейт и мидлвары.

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

import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';

const store = createStore(
  reducer,
  /* preloadedState, */
  compose(
    applyMiddleware(thunk),
    applyMiddleware(logger)
  ),
)

В такой ситуации, в store передается результат функции compose . Последняя, в свою очередь принимает на вход мидлвары.

Теперь мы подобрались к главному. Для редакса написано специальное браузерное расширение Redux DevTools. Установите его в свой браузер.

Ниже код подключения этого экстеншена к хранилищу:

const reduxDevtools = window.__REDUX_DEVTOOLS_EXTENSION__;
const store = createStore(
   reducer,
   /* preloadedState, */
    reduxDevtools && reduxDevtools(),
 );

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

В будущих уроках вам не придется подключать его руками. Мы уже сделали это за вас. Все что нужно - установить расширение и не забывать туда смотреть. Это ваш главный помощник в отладке на протяжении всего курса.

Дополнительные материалы

  1. Официальная документация ReduxDevTools

JS: Redux (React) Reducers

Все что хранится в Store, мы называем состоянием, но не все состояния одинаково полезны. Вот какую классификацию вводит документация Redux:

  • Domain data - данные приложения, которые нужно отображать, использовать и модифицировать. Например список пользователей загруженный с сервера.
  • App state - Данные определяющие поведение приложения. Например текущий открытый URL.
  • UI state - Данные определяющие то как выглядит UI. Например вывод списка в плиточном виде.

Так как Store представляет собой ядро приложения, данные внутри него должны описываться в терминах domain data и app state, но не как дерево компонентов UI. Например такой способ формирования состояния state.leftPane.todoList.todos - плохая идея. Крайне редко дерево компонентов отражается напрямую на структуру состояния и это нормально. Представление зависит от данных, а не данные от представления.

Типичная структура состояния выглядит так:

{
    domainData1 : {}, // todos
    domainData2 : {}, // comments
    appState1 : {},
    appState2 : {},
    uiState1 : {}
    uiState2 : {},
}
Подробнее про работу с состоянием UI будет рассказано в соответствующем уроке

Как уже говорилось в курсе React, структура состояния должна напоминать базу данных. Все максимально плоско и нормализованно.

{
  todos: {
    1: { id: 1, name: 'why?' },
    3: { id: 3, name: 'who?' },
  },
  comments: {
    23: { id: 23, todoId: 3, text: 'great!' },
  },
}

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

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

import { combineReducers, createStore } from 'redux';

const todos = (state = {}, action) => {
  // state is todos part
};

const comments = (state = {}, action) => {
  // state is comments
};

const rootReducer = combineReducers({ todos, comments });
const store = createStore(rootReducer);

Обратите внимание на то что если редьюсер именовать так же как и свойство, то можно написать так: { todos, comments } . В каждый редьюсер приходит state , но это не все состояние контейнера, а только та часть, которая лежит в соответствующем свойстве. Не забудьте про это.

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

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

const todos = (state = {}, action) => {
  switch (action.type) {
    case 'TODO_REMOVE':
      // ...
  }
};

const comments = (state = {}, action) => {
  switch (action.type) {
    // При удалении ToDo нужно удалить все его комментарии
    case 'TODO_REMOVE':
      // ...
  }
};

То есть правильный подход состоит в том чтобы повторять case часть в нужных редьюсерах, а не в том чтобы пытаться получить недостающие части состояния.

Дополнительные материалы

  1. Normalizing State Shape

JS: Redux (React) Redux Actions

Типичный Action выглядит так:

{
  type: 'TODO_ADD',
  payload: {
    /* data */
  }
}

В даже небольших приложениях на js, действий десятки, а то и сотни. В итоге появляется множество одинакового кода. К тому же есть проблема, если ошибиться с именем, хоть при формировании действия или в редьюсере, то система ничего не скажет, придется отлаживать такую ситуацию руками.

По этой причине, появились библиотеки-хелперы, помогающие сократить код. Самой популярной является redux-actions. Она включает в себя два аспекта:

  1. Определение действий
  2. Определение хранилища

Начнем с действий:

import { createAction } from 'redux-actions';
import store from './store';

const addUser = createAction('USER_ADD');
const updateUser = createAction('USER_UPDATE');

store.dispatch(addUser(/* ... */));

Функция createAction формирует новую функцию, которая при вызове возвращает действие (объект { type: ... } ). Эта функция принимает на вход данные, которые попадают в свойство payload .

Подразумевается что сгенерированные функции экспортируются наружу и используются при вызовах store.dispatch() .

Теперь посмотрим редьюсеры:

import { handleActions } from 'redux-actions';
import * as actions from './actions';

const users = handleActions({
  [actions.addUser](state, { payload: { user } }) {
    return { ...state, [user.id]: user };
  },
}, {});

export default combineReducers({
  users,
});

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

  • Каждый обработчик теперь, отдельная функция, а значит не будет проблем с определением одинаковых констант как в случае switch .
  • Не нужно описывать default поведение, когда возвращается сам state
  • Стало невозможным диспатчить неправильное действие, так как каждая функция обработчик формируется на основании функций генерирующих действия.

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

JS: Redux (React) React Redux

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

Начнем с того, что самостоятельно интегрировать редакс не надо, будет больно. Для этой задачи уже написан и, поддерживается командой реакта, пакет react-redux . Он, в свою очередь, вводит новые понятия, которых нет ни в Redux ни в React. К ним относятся:

  • Provider
  • Container

Provider

Начнем с провайдера. Первым делом нужно обернуть корневой React компонент в провайдер:

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux' // импорт компонента!
import TasksContainer from './containers/Tasks';
import store from './store';


render(
  <Provider store={store}>
    <TasksContainer />
  </Provider>,
  document.getElementById('container'),
);

Провайдер исключительно утилитарная сущность. Во-первых это обычный компонент реакта. Во-вторых он хранит внутри себя Store и пробрасывает его нижележащим компонентам. Делается это посредством механизма реакта Context, но об этом можно не знать и не думать.

Container

В связке React и Redux компоненты начинают делиться на два новых типа. Один тип, по сути, это чистая функция работающая только через props, другой тип - так называемые, контейнеры. Они в свою очередь получают данные из Store и распространяют их дальше. Про контейнеры будет отдельный урок.

import { connect } from 'react-redux';
import Tasks from '../components/Tasks.jsx';

const mapStateToProps = state => {
  const props = {
    tasks: state.tasks,
  }
  return props;
};

export default connect(mapStateToProps)(Tasks);

Компонент-контейнер не совсем реален. То есть он существует, но мы его не создаем руками. Этот компонент не содержит виртуального дома и его единственная цель, это взять данные из Store и отдать их в обычный React компонент. Создается он вот таким вызовом: connect(mapStateToProps)(Tasks) . Как видите это функция, которая сначала принимает на вход функцию mapStateToProps и возвращает функцию, в которую передается уже компонент реакта.

Самое главное в этой схеме - mapStateToProps . Эта функция принимает на вход состояние из Store и должна возвратить объект, свойства которого станут props в подключаемом компоненте (в данном случае <Tasks> ). В тривиальном случае мы всегда можем реализовывать эту функцию так state => state . То есть берем и отдаем в компонент все состояние. Но делать так не стоит по многим причинам, начиная от полной просадки производительности, заканчивая тем что появляется сильная зависимость от структуры состояния и лишние данные там где их не ждут. Более того, всю предварительную обработку данных, подготовленных для вывода стоит делать именно здесь. В идеале в компоненты должны попадать уже готовые к выводу данные.

С точки зрения реакта, схема получается следующая:

А вот в результирующем HTML, контейнер следов не оставляет.

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

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux' // импорт компонента!
import TasksContainer from './containers/Tasks';
import store from './store';


render(
  <Provider store={store}>
    <TasksContainer />
  </Provider>,
  document.getElementById('container'),
);

dispatch

Неразобранным остался вопрос работы с Actions. Любой компонент реакта обернутый в контейнерный компонент, в пропсы получает функцию dispatch . Эта функция работает точь в точь как и store.dispatch . Ей нужно передать Action, что в свою очередь запустит цепочку вызовов до перерисовки.

import { addTask } from '../actions';

export default class Tasks extends React.Component {
  addTask = (e) => {
    e.preventDefault();
    this.props.dispatch(addTask({ text: this.props.newTaskText }));
  };

  render() {
    return <div>{/* logic */}</div>;
  }
}

Как мы увидим позже, ту же самую задачу можно делать проще.

Файловая структура

Имея такое количество сущностей, возникает закономерный вопрос о том как их раскладывать в файловой системе. Общепринятой является следующая схема:

actions/index.js
components/App.jsx
containers/App.js
reducers/index.js
index.jsx

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

JS: Redux (React) Containers

Еще одна полезная функция контейнера - оборачивание Actions. Она позволяет обходиться без функции dispatch . Общий принцип работы такой, в файл с контейнером импортируются Actions и передаются вторым параметром в функцию connect .

import { connect } from 'react-redux';
import Component from '../components/TasksList.jsx';
import * as actionCreators from '../actions';

export default connect(
  mapStateToProps,
  actionCreators,
)(Component);

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

Теперь использовать Actions становится гораздо проще:

export default class TasksList extends React.Component {
  removeTask = id => (e) => {
    e.preventDefault();
    this.props.removeTask({ id });
  }

  render() {
    // ...
  }
}

Как видите ничего не надо импортировать, все есть в props . Технически, Actions оборачиваются в другие функции таким образом, что интерфейс работы остается прежним.

Components

Поговорим подробнее о типах компонентов:

  • Presentational
  • Container
Компоненты-представления Компоненты-контейнеры
Назначение Как выглядит (разметка, стили) Как работает (загрузка данных, обновление состояния)
Знают о Redux Нет Да
Читают данные Читают данные из props Подписываются на Redux-состояние
Изменяют данные Вызывают колбеки из props Отправляют Redux-действия
Написаны Руками Обычно генерируются React Redux

Не существует одного единственно верного способа определить, когда использовать презентационные компоненты, а когда компоненты-контейнеры. Понимание придет с опытом, а пока следуйте следующему совету: используйте по-максимуму презентационные компоненты пока это не сложнее, чем с контейнерными компонентами. Когда становится сложнее — вводите компоненты-контейнеры.

Дополнительные материалы

  1. Декораторы вместо контейнеров

JS: Redux (React) Reselect

Еще одна задача которую решают контейнеры - оптимизация. Они автоматически отслеживают то что возвращается из mapStateToProps и если ничего не изменилось с предыдущего рендеринга, то перерисовки не будет. По сути, контейнер ведет себя как PureComponent . Из этого есть несколько следствий:

  • Нужно стараться передавать как можно меньше данных (но не меньше чем нужно).
  • Нужно избегать изменений в mapStateToProps

Но не забывайте что сама функция mapStateToProps выполняется всегда.

Последнее рассмотрим подробнее. Напомню наш код из практики:

const mapStateToProps = ({ tasks }) => {
  const props = {
    tasks: Object.values(tasks),
  };
  return props;
};

На первый взгляд в коде все нормально, но, на самом деле, Object.values создает каждый раз новый объект. Даже если tasks остались прежними. А значит ни о какой эффективности не может быть и речи. Очевидным решением будет перенести эту логику внутрь компонента, но тогда теряется одно из главных преимуществ маппинга. Компоненты завязываются на структуру состояния и выполняют работу по подготовке данных, которая, кстати, начнет дублироваться.

Для решения этой задачи создан пакет reselect . Он позволяет создавать специальные функции “селекторы”, которые выполняют мемоизацию результата. То есть если данные не поменялись, то и результат работы функции будет тем же самым значением или объектом в случае составных данных.

Отмечу что reselect не связан ни с Redux ни с React. Нет никакого слоя интеграции. Селекторы сами по себе и их легко использовать в контейнерах без конфигурации.

Перед тем как создать первый селектор, нужно написать функцию, которая принимает на вход состояние и возвращает нужный срез данных. В нашем случае используется функция getTasks . Затем, с помощью функции createSelector создается селектор. В пример выше в функцию createSelector передается наша исходная функция, и вторая функция, которая производит фильтрацию данных, полученных первой функцией.

Посмотрите на пример ниже:

const getTasks = state => Object.values(state.tasks);

const mapStateToProps = (state) => {
  const props = {
    tasks: getTasks(state),
  };
  return props;
};

В коде выше сразу две ошибки. Во-первых функция извлекающая getTask не селектор, хотя без нее селектор не сделаешь. То есть никакой мемоизации не будет. Во-вторых, ни в коем случае нельзя делать преобразования данных на этом этапе. Именно эту функцию используют селекторы чтобы узнать, а изменились ли данные. Если функция содержит обработку, то данные всегда будут новые и смысл селектора пропадает. Хотя, технически, в коде он останется.

import { createSelector } from 'reselect';

const getTasks = state => state.tasks;
const tasksSelector = createSelector(
  getTasks,
  tasks => Object.values(tasks),
);

const mapStateToProps = (state) => {
  const props = {
    tasks: tasksSelector(state),
  };
  return props;
};

Хорошая новость в том что селекторы можно соединять:

import { createSelector } from 'reselect';

const getTasks = state => state.tasks;
const tasksSelector = createSelector(
  getTasks,
  tasks => Object.values(tasks),
);
const publishedTasksSelector = createSelector(
  tasksSelector,
  tasks => tasks.filter(t => t.state === 'published'),
);
const percentOfFinishedTasksSelector = createSelector(
  publishedTasksSelector,
  (tasks, publishedTasks) => (publishedTasks.length / tasks.length) * 100,
)

const mapStateToProps = (state) => {
  const props = {
    tasks: tasksSelector(state),
    publishedTasks: publishedTasksSelector(state),
    percentOfFinishedTasks: percentOfFinishedTasksSelector(state),
  };
  return props;
};

Как это работает:

  • Селектор вызывает все переданные ему селекторы (которые в свою очередь делают тоже самое и так до самого дна) и собирает результаты их вызовов в массив results
  • Селектор вызывает последнюю переданную функцию как f(...results) . Другими словами, количество аргументов в последней переданной функции селектору, равно количеству селекторов переданных перед этой функцией.
  • То что получилось и есть результат который вернет селектор (а заодно сохранит внутри)

Хотя по началу такая комбинаторика может пугать, в реальности селекторы очень простая вещь. Рекомендую их использовать часто. Кроме самого факта мемоизации, они позволяют переиспользовать выборки в разных компонентах. В файловой системе рекомендуется размещать их по пути selectors/index.js .

JS: Redux (React) UI State

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

Практика показывает, что довольно часто нельзя провести четкую грань между app state и ui state. Начнем с того что отображение на экране зависит вообще от всего. Например в todo приложениях, завершенный todo, как правило, отображается зачеркнутым. В данном случае зачеркнутость определяется тем какое значение в свойстве state у соответствующего todo. Текущий выбранный элемент может быть исключительно элементом UI, а может влиять на поведение программы, например определенных кнопок, позволяющих выполнять действий в стиле “удалить все выбранное”.

  • Открытое модальное окно
  • Подсвеченный текущий элемент
  • Элемент в режиме редактирования
  • Отфильтрованный список
  • Отображение в три колонки

Вот лишь некоторые примеры, которые можно отнести либо к одной либо к другой части состояния.

Так где хранить UI State?

А вот что говорят по этому поводу разработчики React:

The rule of thumb is: do whatever is less awkward.

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

Но чего не стоит делать однозначно, так это примешивать app state и ui state в domain data. Ниже приводится пример правильного разделения:

const state = {
  tasks: {
    1: { /* ... */ },
  },
  tasksUIState: {
    1: { state: 'editing' },
  }
};

JS: Redux (React) Redux Forms

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

import React from 'react';

class Form extends React.Component {
  updateNewTaskText = e =>
    this.props.updateNewTaskText({ text: e.target.value });

  render() {
    const { newTaskText } = this.props;

    return <form action="" className="form-inline">
      <div className="form-group mx-sm-3">
        <input type="text" required
          value={newTaskText} onChange={this.updateNewTaskText} />
      </div>
    </form>;
  }
}

Этого можно избежать подключив библиотеку наподобие redux-form . Документация этого пакета поистинне огромна. Множество вариантов использования и способов кастомизации. Чтобы не сойти с ума, в уроке мы рассмотрим только базовые возможности этой библиотеки.

Как и в случае с самим Redux, подключить ReduxForm целая история. Начнем по порядку:

  1. Автоматизация синхронизации с контейнером подразумевает наличие специального редьюсера:
import { reducer as formReducer } from 'redux-form';

export default combineReducers({
  form: formReducer,
  /* ... */
});
  1. Теперь под каждую форму нужно выделять отдельный компонент и оборачивать его в компонент ReduxForm.
import { reduxForm } from 'redux-form';

class NewTaskForm extends React.Component {
  // ...
}

export default reduxForm({
  form: 'newTask',
})(NewTaskForm);

Обратите внимание на свойство form , оно задает имя ключа, под которым данные текущей формы будут храниться в Redux.
3. Вместо использования стандартных компонентов реакта для элементов формы, ReduxForm поставляется со своими механизмами. Потребность вполне понятная, иначе не сделать автоматическую синхронизацию.

import  { Field } from 'redux-form';

// ...

render() {
  return <form className="form-inline">
    <div className="form-group mx-3">
      <Field name="text" required component="input" type="text" />
    </div>
    <button type="submit" className="btn btn-primary btn-sm">Add</button>
  </form>;
}

Эта часть ReduxForm очень сильно кастомизируется. За подробностями прошу в официальную документацию.
4. Остался последний шаг - отправка формы и работа с ее данными.

class NewTaskForm extends React.Component {
  addTask = (values) => {
    this.props.addTask({ task: values });
  }

  render() {
    return <form onSubmit={this.props.handleSubmit(this.addTask)}>
      {/* ... */}
      <button type="submit" className="btn btn-primary btn-sm">Add</button>
    </form>;
  }
}

ReduxForm прокидывает в формы целую россыпь различных свойств. Главное из них - функция handleSubmit . Ее необходимо вызвать на onSubmit формы, передав туда свой собственный обработчик. После отправки формы, в этот обработчик попадут все значения из формы в виде объекта, где свойство это имя элемента формы.

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

А дальше начинаются нюансы.

Как очистить форму после отправки?

В обработчике отправки можно вызывать функцию this.props.reset() и форма будет сброшена в первоначальный вид.

Как задать параметры по умолчанию?

Достаточно передать в компонент свойство initialValues .

Дополнительные материалы

  1. redux-form

JS: Redux (React) Async Actions

В отличие от синхронных запросов, которые выполняются здесь и сейчас, асинхронные растянуты во времени. Каждый асинхронный запрос можно представить конечным автоматом с тремя состояниями “something requested”, “answer received” или “request failed”. Почему это важно? Как минимум нам важно знать когда запрос был выполнен, чтобы оповестить пользователя и произвести необходимые изменения. Но также важно знать когда запрос начался.

Предположим что пользователь заполнил форму создания новой задачи и нажал “создать”, а потом быстро нажал “создать” еще раз, до того, как запрос успел выполнится. Такая ситуация нередко встречается и с обычными формами, без js. Она приводит к тому, что на сервере создается две одинаковые сущности, либо выскакивают ошибки связанные с валидацией. Правильное решение, в подобной ситуации, связано с необходимостью менять интерфейс так, чтобы повторная отправка стала невозможной. Как правило, все сводится к блокированию кнопки отправки с крутящимся спинером. Соответственно после окончания запроса кнопку необходимо разблокировать, или, если того требует UI, вообще убрать форму. Тоже самое нужно делать не только в случае успеха, но и в случае провала, иначе может получится ситуация что пользовательский запрос провалился, а кнопка осталась заблокирована навсегда (до перезагрузки страницы).

С точки зрения нашего React-Redux приложения, автомат будет состоять из состояния в контейнере и трех событий:

  • TASK_UPDATE_REQUEST
  • TASK_UPDATE_SUCCESS
  • TASK_UPDATE_FAILURE

Именование в стиле request, success и failure рекомендация самого Redux. Желательно всегда придерживаться именно ее в случаях когда состояния три. Большинство запросов укладываются именно в эту схему.

export const updateTaskRequest = createAction('TASK_UPDATE_REQUEST');
export const updateTaskSuccess = createAction('TASK_UPDATE_SUCCESS');
export const updateTaskFailure = createAction('TASK_UPDATE_FAILURE');

Actions описанные выше, по сути синхронны. А где тогда выполняется сам запрос?

Для этого вводится понятие Async Actions. И если в React для работы с асинхронными вызовами ничего дополнительно делать не нужно, то Redux, из коробки, это не умеет. Наиболее простой способ начать выполнять запросы на сервер - подключить библиотеку redux-thunk . Она представляет из себя мидлвару, которую нужно не забыть подключить:

import thunk from 'redux-thunk';

const store = createStore(
  reducers,
  /* ... */,
  applyMiddleware(thunk),
);

На этом интеграция заканчивается.

Следующим шагом создаются сами действия:

;
export const updateTask = (id, values) => async (dispatch) => {
  dispatch(updateTaskRequest());
  try {
    const response = await axios.post(routes.taskUrl(id), { task: values });
    dispatch(updateTaskSuccess({ task: response.data }));
  } catch (e) {
    console.log(e)
    dispatch(updateTaskFailure());
  }
};

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

  1. Уведомляем Redux о начале внешнего запроса
  2. Выполняем внешний запрос
  3. Если запрос выполнился удачно, уведомляем Redux и передаем туда полученные данные (если есть)
  4. Если запрос выполнился неудачно, уведомляем Redux.

Этот паттерн встречается в реальных приложениях крайне часто.

Посмотрим как в коде реакта вызывается обработчик дергающий наш Action:

class EditTaskForm extends React.Component {
  updateTask = (values) => {
    this.props.updateTask(this.props.task.id, values);
  }

  render() {
  const disabled = this.props.taskCreatingState === 'requested';

    return <form action="" className="form-inline" onSubmit={this.props.handleSubmit(this.updateTask)}>
      <div className="form-group mx-3">
        <Field name="text" required component="input" type="text" />
      </div>
      <button type="submit" disabled={disabled} className="btn btn-primary btn-sm">Update</button>
    </form>;
  }
}

export default reduxForm({
  form: 'editTask',
})(EditTaskForm);

Из кода выше видно что действие вызывается как обычно.

redux-thunk всего лишь, один из многих способов работать с асинхронными действиями в Redux. Существуют и другие пакеты, дающие больший контроль и больший уровень автоматизации. Но, как правило, они сложнее в понимании.

3 Likes