Redux — это интересный шаблон, и, по своей сути, он очень прост. Но почему его сложно понять? В этом материале мы рассмотрим базовые концепции Redux и разберёмся с внутренними механизмами хранилищ. Поняв эти механизмы, вы сможете освоиться со всем тем, что происходит, что называется, «под капотом» Redux, а именно — с тем, как работают хранилища, редьюсеры и действия. Это поможет вам вывести на новый уровень отладку приложений, поможет писать более качественный код. Вы будете точно знать, какие именно функции выполняет та или иная строка вашей программы. Мы будем идти к пониманию Redux через практический пример, который заключается в создании собственного хранилища с использованием TypeScript.

image

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

Терминология


Если вы только недавно начали осваивать Redux, или лишь пролистали документацию, вы, наверняка, встретились с некоторыми терминами, которые, полагаю, стоит рассмотреть прежде чем мы приступим к самому главному.

?Действия


Не пытайтесь воспринимать действия (actions) как JavaScript API. У действий есть определённая цель — и это нужно понять в первую очередь. Действия информируют хранилище о намерении (intent).

Работая с хранилищем, ему дают указания, например, что-то вроде этого: «Эй, хранилище! У меня есть к тебе просьба. Пожалуйста, обнови дерево состояния, добавив в него эти данные».

Сигнатура действия, при использовании TypeScript для её демонстрации, выглядит так:

interface Action {
  type: string;
  payload?: any;
}

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

const action: Action = {
  type: 'ADD_TODO',
  payload: { label: 'Eat pizza,', complete: false },
};

Это, по сути, шаблон действия. Но об этом после, а пока — продолжим знакомство с терминологией.

?Редьюсеры


Редьюсер (reducer) — это всего лишь чистая функция, которая принимает состояние (state) приложения (внутреннее дерево состояния, которое хранилище передаёт редьюсеру), и, в качестве второго аргумента, отправленное хранилищу действие. То есть, выглядит всё это так:

function reducer(state, action) {
  //... это было просто
}

Итак, что ещё надо знать о редьюсерах? Редьюсер, как мы знаем, принимает состояние, и для того, чтобы сделать что-нибудь полезное (вроде обновления дерева состояния), нам нужно отреагировать на свойство type действия (мы только что видели это свойство). Делается это обычно с помощью конструкции switch:

function reducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO': {
      // Полагаю, тут надо что-то сделать...
    }
  }
}

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

function reducer(state = {}, action) {
  switch (action.type) {
    case 'ADD_TODO': {
      return {
        ...state,
        // Мы преобразуем, с помощью оператора расширения, существующий массив todos в новый
        // и затем добавляем в конец этого массива новый элемент
        todos: [...state.todos, { label: 'Eat pizza,', complete: false }],
      };
    }
  }

  return state;
}

Обратите внимание на то, что в нижней части кода имеется команда возврата state. Делается это для того, чтобы возвратить исходное состояние в том случае, если в редьюсере нет ветви case, соответствующей некоему действию. Тут же можно добавить, что в качестве первого аргумента здесь добавлена конструкция state = {}. Это — значение параметра по умолчанию. Исходные объекты состояний обычно формируются за пределами редьюсеров, мы ещё об этом поговорим.

Последнее, на что надо обратить внимание — это стремление к иммутабельности. Мы возвращаем совершенно новый объект в каждой ветви case, Он представляет собой комбинацию предыдущего состояния и изменений, внесённых в него. Как результат, на выходе оказывается слегка изменённый вариант первоначального дерева состояния. Тут, сначала, применяется команда …state, оператор расширения, после чего в текущее состояние добавляются новые свойства.

Следуя концепции чистых функций, мы добиваемся того, что одни и те же входные данные всегда приводят к появлению одних и тех же выходных данных. Редьюсеры — чистые функции, которые обрабатывают динамическое состояние на основе действий. Проще говоря, мы настраиваем их, а всё остальное делается в процессе работы. Они инкапсулируют функции, которые содержат логику, необходимую для обновления дерева состояний, основываясь на тех указаниях (действиях), которые мы им передаём.

Редьюсеры — синхронные функции, внутри них следует избегать асинхронного поведения.
Итак, когда же в дело вступает action.payload? В идеале не следует жёстко задавать некие значения в редьюсере, если только это не какие-то простые вещи вроде перевода логического значения из состояния false в состояние true. Теперь, для того, чтобы завершить тему обработки действий, мы используем свойство action.payload, доступное благодаря действию, переданному редьюсеру при его вызове, и получаем необходимые данные:

function reducer(state = {}, action) {
  switch (action.type) {
    case 'ADD_TODO': {
      // получить новые данные
      const todo = action.payload;
      // создать новую структуру данных
      const todos = [...state.todos, todo];
      // вернуть новое представление состояния
      return {
        ...state,
        todos,
      };
    }
  }

  return state;
}

?Хранилище


Мне постоянно приходится видеть, как состояние (state) путают с хранилищем (store). Хранилище — это контейнер, а состояние просто размещается в этом контейнере.

Хранилище — это объект с API, которое позволяет взаимодействовать с состоянием, модифицировать его, читать его значения, и так далее.

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

Мне хотелось бы отметить, что, по сути, функции хранилища заключаются в реализации структурированного процесса обновления свойств в объекте. Собственно говоря, это и есть Redux.

API хранилища


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

const store = new Store(reducers, initialState);

?Метод Store.dispatch()


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

?Метод Store.subscribe()


Метод subscribe позволит организовать передачу в хранилище функций-подписчиков, интересующихся изменениями дерева состояний. Этим функциям, при изменении дерева состояний, передаются соответствующие сведения.

?Свойство Store.value


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

Контейнер хранилища


Как мы уже знаем, хранилище содержит состояние, а так же позволяет нам отправлять ему действия, которые нужно выполнить над деревом состояния. Оно позволяет и подписываться на обновления. Начнём работу над классом Store:

export class Store {
  constructor() {}

  dispatch() {}

  subscribe() {}
}

Пока всё выглядит вполне нормально, но мы забыли об объекте для состояния, state. Добавим его:

export class Store {
  private state: { [key: string]: any };

  constructor() {
    this.state = {};
  }

  get value() {
    return this.state;
  }

  dispatch() {}

  subscribe() {}
}

Мне нравится писать на TypeScript, тут я тоже пользуюсь его механизмами для того, чтобы указать, что объект state будет состоять из строковых ключей, которым могут соответствовать значения любого типа. Это — именно то, что нужно для работы с нашими структурами данных.

Кроме того, тут добавлен метод get value() {}, который возвращает объект state, когда к нему обращаются как к свойству:

console.log(store.value);

Итак, теперь создадим экземпляр хранилища:

const store = new Store();

В данный момент вполне можно вызвать метод dispatch:

store.dispatch({
  type: 'ADD_TODO',
  payload: { label: 'Eat pizza', complete: false },
});

Однако, такой вызов пока ни к чему не приводит, поэтому займёмся работой над методом dispatch, приведём его к такому виду, чтобы ему можно было передать действие:

export class Store {
  // ...
  dispatch(action) {
    // Здесь надо обновить дерево состояния!
  }
  // ...
}

Итак, в методе dispatch надо обновить дерево состояния. Но сначала зададимся вопросом — а как оно выглядит — это дерево состояния?

?Структура данных для хранения состояния


Для целей этого материала структура данных состояния будет выглядеть так:

{
  todos: {
    data: [],
    loaded: false,
    loading: false,
  }
}

Почему? Мы уже знаем, что редьюсеры обновляют дерево состояния. В реальном приложении у нас было бы множество редьюсеров, которые ответственны за обновление некоей части дерева состояния. Эти части нередко называют «слоями» состояния. Каждым таким слоем управляет некий редьюсер.

В данном случае свойство todo в дереве состояний, или слой свойств todo, будет управляться редьюсером. На данный момент наш редьюсер будет работать со свойствами data, loaded, и loading. Здесь используются свойства loaded (загружено) и loading (загружается), так как когда выполняется асинхронная операция, наподобие загрузки JSON по HTTP, нам хотелось бы контролировать различные шаги, которые выполняются в рамках этой операции — от инициации запроса до его успешного завершения.

Продолжим работу над методом dispatch.

?Обновление дерева состояния


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

В этом примере на время забудем о существовании редьюсеров и просто обновим состояние вручную:

export class Store {
  // ...
  dispatch(action) {
    this.state = {
      todos: {
        data: [...this.state.todos.data, action.payload],
        loaded: true,
        loading: false,
      },
    };
  }
  // ...
}

После того, как мы отправим методу dispatch действие 'ADD_TODO', состояние будет выглядеть так:

{
  todos: {
    data: [{ label: 'Eat pizza', complete: false }],
    loaded: false,
    loading: false,
  }
}

Разработка редьюсеров


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

export const initialState = {
  data: [],
  loaded: false,
  loading: false,
};

?Создание редьюсера


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

export function todosReducer(
  state = initialState,
  action: { type: string, payload: any }
) {
  // не забудьте меня вернуть
  return state;
}

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

export function todosReducer(
  state = initialState,
  action: { type: string, payload: any }
) {
  switch (action.type) {
    case 'ADD_TODO': {
      const todo = action.payload;
      const data = [...state.data, todo];
      return {
        ...state,
        data,
      };
    }
  }

  return state;
}

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

Вернёмся к объекту Store:

export class Store {
  private state: { [key: string]: any };

  constructor() {
    this.state = {};
  }

  get value() {
    return this.state;
  }

  dispatch(action) {
    this.state = {
      todos: {
        data: [...this.state.todos.data, action.payload],
        loaded: true,
        loading: false,
      },
    };
  }
}

Нам нужно сделать так, чтобы в хранилище можно было добавлять редьюсеры:

export class Store {
  private state: { [key: string]: any };
  private reducers: { [key: string]: Function };

  constructor(reducers = {}, initialState = {}) {
    this.reducers = reducers;
    this.state = {};
  }

}

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

?Регистрация редьюсера


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

const reducers = {
  todos: todosReducer,
};

const store = new Store(reducers);

Тут происходит самое интересное, и обычно непонятное. А именно, здесь свойство todos становится результатом операции вызова хранилищем редьюсера todosReducer, который как мы знаем, возвращает новое состояние, основываясь на некоем действии.

?Вызов редьюсеров в хранилище


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

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

export class Store {
  // ...
  dispatch(action) {
    this.state = this.reduce(this.state, action);
  }

  private reduce(state, action) {
// найти и возвратить новое состояние
    return {};
  }
}

Когда мы передаём в хранилище действие, мы, фактически, вызываем метод reduce класса Store, который только что создали, и передаём ему состояние и действие. Эта конструкция называется корневым редьюсером. Можно заметить, что он принимает state и action — так же, как делает и todosReducer.

Теперь поговорим о приватном методе reduce, так как это — самый важный шаг по построению дерева состояния и по сведению воедино всего, о чём мы тут говорим.

export class Store {
  private state: { [key: string]: any };
  private reducers: { [key: string]: Function };

  constructor(reducers = {}, initialState = {}) {
    this.reducers = reducers;
    this.state = {};
  }

  dispatch(action) {
    this.state = this.reduce(this.state, action);
  }

  private reduce(state, action) {
    const newState = {};
    for (const prop in this.reducers) {
      newState[prop] = this.reducers[prop](state[prop], action);
    }
    return newState;
  }
}

Вот что здесь происходит:

  • Мы создаём объект newState, который будет содержать новое дерево состояния.
  • Мы перебираем объект this.reducers, зарегистрированный в хранилище.
  • Мы, в редьюсере, переносим свойства из todos, в newState.
  • Мы обращаемся к каждому из редьюсеров, по одному, и вызываем его, передавая слой состояния (через state[prop]) и действие

Значение prop в данном случае — это просто todos, поэтому всё это можно рассматривать так:

newState.todos = this.reducers.todos(state.todos, action);

?Обработка initialState с помощью редьюсера


Теперь осталось лишь поговорить об объекте initialState. Если мы собираемся использовать запись вида Store(reducers, initialState) для подготовки исходного состояния всего хранилища, нам нужно обработать его редьюсером в ходе создания хранилища:

export class Store {
  private state: { [key: string]: any };
  private reducers: { [key: string]: Function };

  constructor(reducers = {}, initialState = {}) {
    this.reducers = reducers;
    this.state = this.reduce(initialState, {});
  }

  // ...
}

Помните о том, как мы рассказывали, что в конце кода каждого редьюсера должна быть команда вида return state? Теперь вы знаете — почему. У нас это есть для того, чтобы можно было передать в качестве действия пустой объект, {}, подразумевая, что при этом ветки оператора switch будут пропущены, и в результате у нас окажется дерево состояния, полученное через constructor.

Механизмы работы с подписчиками


Вы часто будете сталкиваться с термином «подписчик» в мире обозреваемых объектов, где каждый раз, когда обозреваемый объект генерирует новое значение, нас уведомляют об этом через подписку. Подписка — это нечто вроде просьбы: «дай мне данные, когда они окажутся доступными или изменятся».

В нашем случае работа с механизмами подписки будет выглядеть так:

const store = new Store(reducers);

store.subscribe(state => {
  // сделать что-нибудь со`state`
});

?Подписчики хранилища


Добавим в хранилище ещё несколько свойств, позволяющих настроить механизм подписки:

export class Store {
  private subscribers: Function[];

  constructor(reducers = {}, initialState = {}) {
    this.subscribers = [];
    // ...
  }

  subscribe(fn) {}

  // ...
}

Здесь имеется метод subscribe, который теперь принимает функцию (fn) как аргумент. Теперь нам нужно передать каждую такую функцию в массив subscribers:

export class Store {
  // ...

  subscribe(fn) {
    this.subscribers = [...this.subscribers, fn];
  }

  // ...
}

Это, как видите, было просто. А где же можно сообщить нашим подписчикам о том, что что-то изменилось? Конечно, в методе dispatch!

export class Store {
  // ...

  get value() {
    return this.state;
  }

  dispatch(action) {
    this.state = this.reduce(this.state, action);
    this.subscribers.forEach(fn => fn(this.value));
  }
  // ...
}

И это, опять же, просто. Каждый раз, когда мы вызываем dispatch, мы передаём методу reduce состояние и обходим подписчиков, передавая им this.value (помните о том, что тут срабатывает геттер value).

Теперь нам осталось решить лишь одну задачу. Когда мы вызываем .subscribe(), мы не хотим (в этот конкретный момент) получать значение state. Мы хотим получить его после выполнения метода dispatch. Поэтому примем решение информировать новых подписчиков о текущем состоянии как только они подпишутся:

export class Store {
  // ...

  subscribe(fn) {
    this.subscribers = [...this.subscribers, fn];
    fn(this.value);
  }

  // ...
}

Тут мы берём переданную через метод subscribe функцию и, после оформления подписки, вызываем её с передачей ей дерева состояния.

?Отписка от хранилища


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

Всё, что тут нужно сделать — это вернуть замыкание, которое, будучи вызванным, удалит функцию из списка подписчиков:

export class Store {
  // ...

  subscribe(fn) {
    this.subscribers = [...this.subscribers, fn];
    fn(this.value);
    return () => {
      this.subscribers = this.subscribers.filter(sub => sub !== fn);
    };
  }

  // ...
}

Тут мы используем ссылку на функцию, перебираем подписчиков, проверяем, не равен ли текущий подписчик нашему fn. Далее, с помощью Array.prototype.filter, то, что больше не нужно, удаляется из массива подписчиков. Пользоваться этим можно так:

const store = new Store(reducers);

const unsubscribe = store.subscribe(state => {});

destroyButton.on('click', unsubscribe, false);

И это всё, что нам нужно.

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

Полный код хранилища


Вот полный код того что мы сделали:

export class Store {
  private subscribers: Function[];
  private reducers: { [key: string]: Function };
  private state: { [key: string]: any };

  constructor(reducers = {}, initialState = {}) {
    this.subscribers = [];
    this.reducers = reducers;
    this.state = this.reduce(initialState, {});
  }

  get value() {
    return this.state;
  }

  subscribe(fn) {
    this.subscribers = [...this.subscribers, fn];
    fn(this.value);
    return () => {
      this.subscribers = this.subscribers.filter(sub => sub !== fn);
    };
  }

  dispatch(action) {
    this.state = this.reduce(this.state, action);
    this.subscribers.forEach(fn => fn(this.value));
  }

  private reduce(state, action) {
    const newState = {};
    for (const prop in this.reducers) {
      newState[prop] = this.reducers[prop](state[prop], action);
    }
    return newState;
  }
}

Как видите, всё не так уж и сложно.

Итоги


Вполне возможно, что вы слышали обо всех тех механизмах, о которых мы сегодня говорили, или даже использовали их, но не интересовались тем, как они устроены. Надеюсь, создав собственное хранилище, вы поняли, как работает всё то, из чего оно состоит. В работе действий и редьюсеров нет ничего таинственного. Метод dispatch сообщает хранилищу о необходимости выполнить процесс определения нового состояния путём вызова каждого редьюсера и попытки сопоставления action.type с одной из ветвей оператора switch. А дерево состояния — это итоговое представление того, что получается после вызова всех редьюсеров.

Я, благодаря примеру, которым поделился с вами, наконец понял Redux. Надеюсь, он поможет в этом и вам.

Уважаемые читатели! Как вы осваиваете новые технологии?

Комментарии (18)


  1. x512
    22.12.2017 16:27

    Почти все описания Redux начинаются со слов «управление состоянием в современных приложениях — сложная штука, поэтому мы придумали Redux, чтобы решить её» Вот, к примеру, здесь. Т.е. первым предложением создается проблема, а следующим предлагается великолепное решение. И решение выглядит как серебрянная пуля, которая необходима всем разработчикам SPA. Очень хочется увидеть пример, где демонстрируются, а не декларируются преимущества Redux и описываются области применения.


    1. Fen1kz
      22.12.2017 17:52

      Интересно, в каком виде?

      Статья — делаем приложение Х на redux и без него! Вот без редукса *тык тык тык, код* посмотрите как неудобно, вот с ним *тык тык тык код* посмотрите как круто и удобно!

      Как нибудь так или по-другому?


      1. x512
        22.12.2017 17:59

        Именно, что можно было понять что именно неудобно.
        PS: круто если пример будет на Angular т.к его ChangeDetection способствует упрощению кода


    1. frog
      22.12.2017 18:18
      +1

      Мне представляется, что сильная сторона Redux в том, что он вынуждает определённым образом подходить к созданию приложения (иначе просто ничего работать толком не будет). Причём, полезность этого подхода на простых примерах продемонстрировать крайне сложно, так как на вид всё получается сильно сложнее, чем без него.
      То, что статьи про Redux действительно часто сходу акцентируют внимание на том, как он устроен, а не на том, какую проблему решает — это да, есть такое.


      1. NiTr0_ua
        22.12.2017 21:37

        не вынуждает. более того — он вынуждает делать трюки в стиле «почеши ногой за ухом».

        простой пример: есть допустим форма. в ней есть текстовое поле ввода, в ней есть кнопка сабмит.
        каноничный вариант ее обработки — по клику на сабмит получаем контент текстового поля и делаем с ним все, что нужно. логично же?

        redux-way — вешаемся на ивент смены содержимого этого поля, и при КАЖДОМ вводе символа ивентом обновляем внутреннюю переменную, содержащую контент поля. а потом по клику на сабмит — шлем ивент, мол, сабмит кликнут, эй, редьюсер, ану-ка обработай это.

        да, я понимаю зачем это сделано (чтобы можно было без проблем этот же react+redux натянуть хоть на js+html, хоть на java+android native app, хоть на черта лысого с минимумом правок) — но, блин, джуниорам за такое в других языках/фреймворках руки еще на буткемпе отрывают. ибо дикий оверхид.

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


        1. ookami_kb
          22.12.2017 23:43

          и да, второй огромный минус редакса невозможно в редьюсере вызвать новый ивент.

          Это не минус, это плюс. Ну или, по крайней мере, один из основополагающих принципов. Редьюсер должен изменять состояние хранилища в ответ на action. Всё. Для всего остального можно прикручивать обработку непосредственно в action, или написать middleware, в зависимости от задачи.


        1. VolCh
          24.12.2017 12:43

          redux-way не заставляет вас каждый символ обрабатывать, это заставляет делать react-way, согласно которому dom (в общем случае любое представление) есть чистая функция от состояния.


        1. HelpOP
          25.12.2017 15:06

          джуниорам за такое в других языках/фреймворках руки еще на буткемпе отрывают

          второй огромный минус редакса невозможно в редьюсере вызвать новый ивент

          Тут должен возникнуть когнитивный диссонанс.
          Давайте по порядку. Будет ли юзер одновременно писать текст больше чем в одно поле? Уже проблема кажется глупой в силу того что производительность не теряется, а удобство возрастает.
          Мне не нужно думать где какие поля у меня созданы все есть в props.
          Нет проблемы асинхронного доступа так как dispatch синхронный.
          Если мне нужно где-то продублировать вводимые данные они mapStateToProps мне их вернет(опять таки мне вообще не нужно думать о том как туда эти данные попадут).


          1. NiTr0_ua
            25.12.2017 15:15

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

            как это — «не теряется»?

            у вас есть поле с NNNNкб текста. на каждое изменение (добавление буквы) — все эти NNNNкб копируются в ивент, потом — пишутся из ивента в storage, потом — из storage в props. при этом — объект в storage пересоздается.

            иногда мне очень хочется посадить реакт/редакс разработчика за какой-нить атом N450 и заставить попользоваться созданным ним продуктом…

            Если мне нужно где-то продублировать вводимые данные они mapStateToProps мне их вернет(опять таки мне вообще не нужно думать о том как туда эти данные попадут).

            а если надо элементарно после получения ивента от стороннего компонента выполнить операцию, создающую новый ивент? это приходится пихать в пустой компонент который где-то лепить на странице…


            1. HelpOP
              25.12.2017 15:25

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


              1. faiwer
                25.12.2017 16:10

                Собственно в этом и проблема. Совершенно рядовая задача не имеет ни одного изящного красивого решения. Впрочем в мутабельном мире с этим тоже не всё хорошо (вспоминаю knockout-validation).


            1. faiwer
              25.12.2017 16:08

              потом — из storage в props

              А тут самое вкусное место — mapStateToProps()-ы вызываются всегда и у всех connect-компонент на любое изменение store. В зависимости от приложения и понимания человеком архитектуры, таких мест может быть очень много.


  1. Fen1kz
    23.12.2017 15:26

    > простой пример: есть допустим форма. в ней есть текстовое поле ввода, в ней есть кнопка сабмит.
    каноничный вариант ее обработки — по клику на сабмит получаем контент текстового поля и делаем с ним все, что нужно. логично же?

    redux-way — вешаемся на ивент смены содержимого этого поля, и при КАЖДОМ вводе символа ивентом обновляем внутреннюю переменную, содержащую контент поля. а потом по клику на сабмит — шлем ивент, мол, сабмит кликнут, эй, редьюсер, ану-ка обработай это.

    Вот зачем? По клику на сабмит получаем контент текстового поля и диспатчим экшен.


    1. NiTr0_ua
      25.12.2017 15:23

      посмотрите код redux-form что ли.

      а ваш метод — это ни разу не react/redux-way.


  1. Frankfurt
    24.12.2017 11:19

    Я всё равно не понял


  1. Voronar
    24.12.2017 12:44

    Чтобы понять redux нужно просто написать большое приложение.
    Сначала оно будет тормозить. В процессе оптимизации вы поймёте Redux ещё больше.


  1. yesasha
    25.12.2017 15:06

    Когда я пойму что это такое, я тоже напишу статью! А ещё мне кажется, что в нескольких своих проектах я сам пришёл к чему-то подобному. Например когда делал рисовалку на канвасе и решил сделать «undo». Тоже самое с текстовым редактором. Каждый раз когда хочешь следить за историей изменений, приходишь к чему-то подобному. Или то что у меня получалось небыло редуксом в полной степени?


    1. VolCh
      25.12.2017 17:08

      История изменений напрямую к redux отношения не имеет, сам он её не обеспечивает, он лишь возвращает новый стейт в ответ на экшен. Два основных подхода к истории: хранить "снэпшоты" состояния и хранить начальное состояние и все события. Redux позволяет относительно просто и то, и другое реализовать, благодаря своей функциональной чистоте, причём с хранением истории как внутри хранилища, так и снаружи.