Redux: Каковы недостатки хранения всего вашего состояния в одном неизменяемом атоме?

Созданный на 10 февр. 2016  ·  91Комментарии  ·  Источник: reduxjs/redux

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

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

Мне просто любопытно, потому что это применимо не только к redux, но в некоторой степени и к другим вещам, таким как Om , datomic и которые говорят о выворачивании базы данных наизнанку . Мне нравится redux, мне нравится Om, я интересуюсь данными, и мне очень понравился этот разговор.

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

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

Поскольку вы, ребята, вероятно, обдумали это гораздо больше, чем я, не могли бы вы мне помочь?

discussion docs question

Самый полезный комментарий

Redux - это, по сути, сбор событий, когда есть одна проекция для использования.

В распределенных системах обычно есть один журнал событий (например, Kafka) и несколько потребителей, которые могут проецировать / сокращать этот журнал в несколько баз данных / хранилищ, которые размещены на разных серверах (обычно реплика БД фактически является редуктором). Таким образом, в распределенном мире нагрузка и использование памяти ... распределены, в то время как в Redux, если у вас есть сотни виджетов, каждый из которых имеет свое локальное состояние, все это выполняется в одном браузере, и каждое изменение состояния имеет некоторые накладные расходы из-за неизменности обновления данных.

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

Рендеринг этого состояния сверху, по моему опыту, неудобен и к тому же не всегда достаточно производительный (по крайней мере, с React, который, вероятно, не является самой быстрой реализацией VDom). Redux решает эту проблему с помощью connect но, тем не менее, по мере роста количества подключений это может стать узким местом. Есть решения

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

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


Я также согласен с @jquense

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

Для монтирования любого состояния в хранилище Redux требуется больше шаблонов. Архитектура Elm, вероятно, решает эту проблему более элегантно, но также требует большого количества шаблонов.

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

  • Ввод текста
  • Положение прокрутки
  • Размер области просмотра
  • Положение мыши
  • Положение / выбор каретки
  • Данные холста
  • Несериализуемое состояние
  • ...

Все 91 Комментарий

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

Большое спасибо! Я действительно хотел услышать об этом.

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

Это своего рода сложный вопрос. Такие проекты, как OM и redux, и другие библиотеки атомов с одним состоянием, активно смягчают недостатки. Без ограничения неизменяемости и стробированного контролируемого доступа атом с одним состоянием ничем не отличается от присоединения всех ваших данных к window (недостатки которого хорошо известны)

Однако, что касается подхода Redux, самым большим недостатком для нас является то, что атомы с одним состоянием - это что-то вроде «все или ничего». Преимущества легкой гидратации, моментальных снимков, путешествий во времени и т. Д. Работают только в том случае, если нет _не_ другого места, где живет важное государство. В контексте React это означает, что вам нужно сохранить состояние, которое должным образом принадлежит компонентам в Магазине, иначе вы потеряете множество преимуществ. Если вы действительно хотите поместить все в Redux, это часто оказывается обременительным и многословным и добавляет раздражающий уровень косвенности.

Однако ни один из этих недостатков не был для нас особенно запретительным :)

Итак, я присматривался к Redux и этому стилю архитектуры для моделирования состояний игры; Я заинтересован в создании симулированных миров в стиле Civ и более простых полностью записанных историй настольных игр в контексте браузера, что, возможно, является необычным вариантом использования для некоторых, но я сомневаюсь, что кто-то захочет, чтобы я тоже останавливался.

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

То, с чем я боролся, как новый пользователь, больше всего пытается преодолеть как крайнее (imho) изменение стиля кодирования в Redux, так и концептуальные изменения одновременно; это остается немного сложным даже сейчас, через месяц и изменится после попытки глубоко погрузиться в это; Я просматриваю https://github.com/ngrx/store рядом, чтобы разобраться в концепциях, но с более знакомым стилем / синтаксисом и другими подразумеваемыми / предоставленными фреймворками.

Я понимаю, что для некоторых фреймворк - это костыль; но некоторым из нас они нужны; Немногие люди будут настолько успешны, чтобы иметь полезные твердые мнения, поэтому фреймворки лучше, чем возиться в темноте, понимаете? Так что это моя точка зрения, программист среднего уровня, среднего возраста, новичок в концепциях :)

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

Это не твоя вина, но проблема, которую я бы хотел решить :)

Я думаю, что самый большой вопрос, который у меня все еще есть, заключается в том, куда должны идти любые несериализуемые элементы, такие как функции, экземпляры или обещания? Задумывался об этом в Reactiflux прошлой ночью и не получил хороших ответов. Также только что видел, как кто-то опубликовал http://stackoverflow.com/questions/35325195/should-i-store-function-references-in-redux-store .

Пример использования для нескольких магазинов (имейте в виду, это лучший вариант, о котором я мог подумать):

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

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

@gaearon мы создаем торговую платформу. Я уже однажды объяснял вам, почему один магазин нам не подходит (после митапа React.js в Санкт-Петербурге). Я попытаюсь объяснить это еще раз подробно (в блоге на среднем уровне?), Но мне, вероятно, понадобится помощь из-за моего знания английского :) Можно, если я отправлю его вам или кому-то еще здесь для просмотра в Twitter напрямую сообщения, когда это будет сделано? Я не могу назвать точную дату, но постараюсь написать этот пост в ближайшее время (скорее всего, в конце этого месяца).

И да, мы все еще используем Redux с несколькими хранилищами, нарушая некоторые правила из документов, как сказал @timdorr (за счет этого мы не можем использовать react-redux так же удобно, если нам нужны данные из разных хранилищ, как есть в случае единственного магазина)

Redux - это, по сути, сбор событий, когда есть одна проекция для использования.

В распределенных системах обычно есть один журнал событий (например, Kafka) и несколько потребителей, которые могут проецировать / сокращать этот журнал в несколько баз данных / хранилищ, которые размещены на разных серверах (обычно реплика БД фактически является редуктором). Таким образом, в распределенном мире нагрузка и использование памяти ... распределены, в то время как в Redux, если у вас есть сотни виджетов, каждый из которых имеет свое локальное состояние, все это выполняется в одном браузере, и каждое изменение состояния имеет некоторые накладные расходы из-за неизменности обновления данных.

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

Рендеринг этого состояния сверху, по моему опыту, неудобен и к тому же не всегда достаточно производительный (по крайней мере, с React, который, вероятно, не является самой быстрой реализацией VDom). Redux решает эту проблему с помощью connect но, тем не менее, по мере роста количества подключений это может стать узким местом. Есть решения

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

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


Я также согласен с @jquense

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

Для монтирования любого состояния в хранилище Redux требуется больше шаблонов. Архитектура Elm, вероятно, решает эту проблему более элегантно, но также требует большого количества шаблонов.

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

  • Ввод текста
  • Положение прокрутки
  • Размер области просмотра
  • Положение мыши
  • Положение / выбор каретки
  • Данные холста
  • Несериализуемое состояние
  • ...

Этот вопрос, который я только что видел на SO:

http://stackoverflow.com/questions/35328056/react-redux-should-all-component-states-be-kept-in-redux-store

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

Кроме того, это может быть помехой, если вы пытаетесь реализовать что-то вроде этого https://github.com/ericelliott/react-pure-component-starter, в котором каждый компонент является чистым и не имеет права голоса в своем собственном состоянии. .

Мне было трудно использовать единое дерево состояний redux, когда мне нужно было управлять данными нескольких сеансов. У меня было произвольное количество ветвей идентичной формы, и у каждой было несколько уникальных идентификаторов (в зависимости от того, откуда пришли данные, вы бы использовали другой ключ). Простота функций редуктора и функций повторного выбора быстро исчезла при столкновении с этими требованиями - необходимость писать пользовательские геттеры / сеттеры для нацеливания на конкретную ветвь казалась слишком сложной и плоской в ​​простой и совместимой среде. Я не знаю, является ли несколько хранилищ лучшим вариантом для удовлетворения этого требования, но было бы неплохо использовать некоторые инструменты для управления сеансами (или любыми другими данными идентичной формы) в едином дереве состояний.

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

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

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

Некоторые причины, по которым состояние анимации неудобно для Redux:

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

С другой стороны, определенно существуют проблемы с построением состояния анимации полностью вне дерева состояний Redux.

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

  • virtual-dom diffs не будет учитывать ваши манипуляции с анимацией, такие как пользовательские стили, которые вы устанавливаете на узле - если вы устанавливаете некоторые стили на узле DOM, вы должны удалить их самостоятельно, если хотите, чтобы они исчезли
  • Если вы выполняете анимацию в самом документе, вы должны быть очень осведомлены об обновлениях виртуального домена - вы можете запустить анимацию на одном узле DOM, только чтобы обнаружить, что содержимое этого узла DOM было изменено в середине анимации.
  • Если вы выполняете анимацию в самом документе, вы должны опасаться перезаписи стилей и атрибутов виртуального домена, которые вы установили на своем узле, и вы должны опасаться перезаписи стилей и атрибутов, установленных виртуальным домом.

И если вы попытаетесь позволить virtual-dom позаботиться обо всех манипуляциях с DOM (что вам следует!), Но без сохранения состояния анимации в Redux, вы столкнетесь с такими сложными проблемами, как эти:

  • Как вы показываете состояние анимации своим компонентам? Локальное состояние в ваших компонентах? Какое-то другое глобальное государство?
  • Обычно ваша логика состояния находится в редукторах Redux, но теперь вам нужно добавить много логики рендеринга непосредственно в ваши компоненты для того, как они будут анимироваться в ответ на ваше состояние. Это может быть довольно сложно и многословно.

В настоящее время есть отличные проекты, такие как react-motion, которые делают большие успехи в решении этой проблемы _ для React_, но Redux не является эксклюзивным для React. Я чувствую, что этого не должно быть - многие люди приносят свой собственный уровень представления и пытаются интегрироваться с Redux.

Для всех, кому интересно, лучшее решение, которое я нашел для Redux + virtual-dom, - это сохранить два атома состояния: Redux сохраняет основное состояние приложения, содержит базовую логику для управления этим состоянием в ответ на действия и т. Д. Другое состояние - изменяемый объект, содержащий состояние анимации (я использую mobservable). Уловка состоит в том, чтобы подписаться на оба изменения состояния Redux и изменения состояния анимации, а также визуализировать пользовательский интерфейс как функцию обоих:

/* Patch h for jsx/vdom to convert <App /> to App() */
import h from './h'
import { diff, patch, create } from 'virtual-dom'
import { createStore } from 'redux'
import { observable, autorun } from 'mobservable'
import TWEEN from 'tween.js'
import rootReducer from './reducers'
import { addCard } from './actions'
import App from './containers/App'

// Redux state
const store = createStore()

// Create vdom tree
let tree = render(store.getState())
let rootNode = create(tree)
document.body.appendChild(rootNode)

// Animation observable
let animationState = observable({
  opacity: 0
})

// Update document when Redux state 
store.subscribe(function () {
  // ... anything you need to do in response to Redux state changes
  update()
})

// Update document when animation state changes
autorun(update)

// Perform document update with current state
function update () {
  const state = store.getState()
  let newTree = render(state, animationState)
  let patches = diff(tree, newTree)
  rootNode = patch(rootNode, patches)
  tree = newTree
}

// UI is a function of current state (and animation!)
function render (state, animation = {}) {
  return (
    <App {...state} animation={animationState} />
  )
}

// Do some animations
function animationLoop (time) {
  window.requestAnimationFrame(animationLoop)
  TWEEN.update(time)
}
animationLoop()

new TWEEN.Tween(animationState)
      .to({ opacity: 100 }, 300)
      .start()

// Or when you dispatch an action, also kick off some animation changes...
store.dispatch(addCard())
/* etc... */

Спасибо всем за отличные отзывы! Держите их.

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

@taggartbg

Мне было трудно использовать единое дерево состояний redux, когда мне нужно было управлять данными нескольких сеансов. У меня было произвольное количество ветвей идентичной формы, и у каждой было несколько уникальных идентификаторов (в зависимости от того, откуда пришли данные, вы бы использовали другой ключ). Простота функций редуктора и функций повторного выбора быстро исчезла при столкновении с этими требованиями - необходимость писать пользовательские геттеры / сеттеры для нацеливания на конкретную ветвь казалась слишком сложной и плоской в ​​простой и совместимой среде.

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

@istarkov

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

Не могли бы вы объяснить подробнее, как это происходит? Обычно мы предлагаем вам запускать только один редуктор на любом срезе состояния. Как могут происходить столкновения? Вы делаете несколько проходов по штату? Если да, то почему?

@gaearon @istarkov : возможно, имеется в виду, что различные плагины и связанные библиотеки могут бороться за одно и то же пространство имен верхнего уровня? Библиотеке A нужен верхний ключ с именем myState, но то же самое и для библиотеки B и т. Д.

Да, это хороший аргумент, даже если

@gaearon

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

Буду рада! Я сделаю это, когда у меня будет время.

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

Я был бы счастлив поговорить об этом вне форума, я вполне мог ошибаться.

@taggartbg : совершенно не зная, как здесь выглядит ваш код. Вы говорите о попытках разобраться с состоянием, которое выглядит так?

{ groupedData : { first : {a : 1, b : 2}, second : {a : 3, b : 4}, third : {a : 5, b, 6} }

Похоже, вы могли бы справиться с этим на стороне редуктора, имея единственную функцию редуктора, которая принимает ключ идентификатора группы как часть каждого действия. На самом деле, смотрели ли вы на что-то вроде https://github.com/erikras/multireducer , https://github.com/lapanoid/redux-delegator , https://github.com/reducks/redux-multiplex или https://github.com/dexbol/redux-register?

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

@gaearon

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

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

Однако с точки зрения разработчика с небольшим опытом или без опыта работы с React, который хочет применить функциональный подход к пользовательскому интерфейсу, может быть неочевидно, какое состояние принадлежит Redux. Я выполнил несколько проектов, в которых сначала было достаточно Redux и virtual-dom, но по мере роста сложности приложения возникла необходимость в этой другой «абстракции эфемерного состояния». Это не всегда очевидно, когда вы в первый раз добавляете редуктор анимации с некоторыми базовыми флагами анимации, но позже становится довольно неприятно.

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

РЕДАКТИРОВАТЬ: название проблемы: «Каковы недостатки хранения всего вашего состояния в одном неизменяемом атоме?» Это «все ваше состояние в одном неизменяемом атоме» - это именно та формулировка, которая в конечном итоге приводит к получению большого количества неправильных состояний в дереве Redux. В документации может быть несколько явных примеров, которые помогут разработчикам избежать подобной ловушки.

@gaearon У нас на Cycle.js состоялся интересный разговор об архитектурных различиях между состоянием одного атома и пиппингом. ЗДЕСЬ .

Я знаю, что это не на 100% связано с Redux, но я хотел дать другую перспективу по сравнению с реализацией, которая использует наблюдаемые потоки в качестве потока данных вокруг приложения (для Cycle приложение представляет собой набор чистых функций).

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

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

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

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

Заранее извините, если это полностью вырвано из контекста, хотел продемонстрировать, как в другой парадигме был подобный разговор: smiley:

@timdorr

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

Я думаю, что лучше, когда у виджета есть собственное состояние (возможно, даже его собственное (сокращенное?) Хранилище), чтобы он мог работать автономно в любом (mashup-) приложении и получать только некоторые из его свойств. Подумайте о погодном виджете. Он может извлекать и отображать данные самостоятельно и получать только такие свойства, как город, высота и ширина. Другой пример - виджет Twitter.

Отличное обсуждение!

В основном мне неудобно комбинировать несколько приложений / компонентов / плагинов.

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

Например:

Я создаю мессенджер, похожий на iMessage. Он имеет состояние редукции currentConversationId и т. Д. Мой компонент Messenger имеет @connect(state => ({ currentConversationId: state.messenger.currentConversationId })) .

Я хочу включить этот Messenger в новое приложение. Мне нужно будет import { rootReducer as messengerRootReducer } from 'my-messenger' и добавить его к combineReducers({ messenger: messengerRootReducer }) чтобы он заработал.

Теперь, если я хочу иметь два экземпляра <Messenger> в приложении с разными данными, я не могу, потому что я привязан к state.messenger в компоненте Messenger. Изготовлениеработа с определенным сегментом магазина заставит меня использовать настроенный @connect .

Кроме того, предположим, что содержащее приложение имеет определенное имя действия, которое существует в модуле my-messenger , произойдет столкновение. Вот почему большинство плагинов redux имеют префиксы типа @@router/UPDATE_LOCATION .

Несколько случайных / безумных идей:

1

@connect может подключаться к определенному сегменту магазина, поэтому я могу включить в свое приложение несколько <Messenger> которые подключаются к их собственному сегменту данных, например state.messenger[0] / state.messenger[1] . Он должен быть прозрачным для <Messenger> который все еще продолжает подключаться к state.messenger , однако содержащее его приложение может предоставить фрагмент примерно так:

@connect(state => ({ messengers: state.messengers }))
class App extends Component {
  render() {
    return (
      <div>
        {this.props.messengers.map(messenger =>
          <ProvideSlice slice={{messenger: messenger}}><Messenger /></ProvideSlice>
        }
      </div>
    )
  }
}

При использовании глобальных нормализованных сущностей возникает проблема, также необходимо присутствие state.entities , поэтому вместе с нарезанным состоянием должно каким-то образом присутствовать все состояние. ProvideSlice может установить некоторый контекст, из которого Messenger @connect сможет читать.

Другая проблема заключается в том, как связать действия, запускаемые компонентами в <Messenger> влияют только на этот фрагмент. Возможно, @connect ниже ProvideSlice запускают mapDispatchToProps действия только в этом сегменте состояния.

2

Объедините определения store и reducer , так что каждый редуктор также может быть автономным. Магазин звучит как контроллер, а редуктор - это представление. Если мы воспользуемся подходом React, «все является компонентом» (в отличие от контроллера / директивы angular 1.x), он может позволить компонентам и их состоянию / действиям действительно быть автономными. Для таких вещей, как нормализованные сущности (например, «глобальное состояние») можно использовать что-то вроде контекста React, и он может быть доступен через все действия (например, getState в thunk ) / подключенные компоненты.

@elado Самая умная идея, которую я видел на данный момент, - это проектирование с учетом «провайдеров»: https://medium.com/@timbur/react -automatic-redux-sizes-and-replicators-c4e35a39f1

Спасибо @sompylasar. Я немного в разъездах, но когда у меня появится возможность, я планирую написать еще одну статью, описывающую поставщиков более кратко, для тех, кто уже знаком с React и Redux. Любой, кто уже знаком, должен найти это безумно простым / легким. :)

@gaearon все равно хотел бы услышать ваше

Больше всего меня беспокоит то, что компоненты контейнера сложнее составлять, повторно использовать, вкладывать и вообще перемещать, потому что существует две независимые иерархии одновременно (представления и редукторы). Также не совсем понятно, как писать повторно используемые компоненты, которые либо используют Redux в качестве детали реализации, либо хотят предоставить удобный для Redux интерфейс. (Есть разные подходы.) Я также не впечатлен тем, что каждое действие должно идти «полностью» вверх, а не где-то замыкаться. Другими словами, я хотел бы видеть что-то вроде модели локального состояния React, но с поддержкой редукторов, и я хотел бы, чтобы она была очень практичной и настраивалась на реальные варианты использования, а не на красивую абстракцию.

@gaearon есть идеи, как начать реализацию такой системы?

@gaearon

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

Я думаю, что сложность заключается в том, что локальное состояние React привязано к жизненному циклу компонента, поэтому, если вы попытаетесь смоделировать локальное состояние в Redux, у вас будет учетная запись для «монтирования» и «размонтирования». У Elm нет этой проблемы, потому что 1) компоненты не имеют жизненных циклов и 2) «монтирование» представления является производным от модели, тогда как в React локальное состояние существует только после того, как представление было смонтировано. (править: точнее сказать прямо перед установкой, но после того, как решение о монтировании уже было принято)

Но, как вы упомянули, архитектура Elm, идущая «полностью» вверх, имеет свои недостатки и не очень хорошо сочетается с циклом обновления и согласования React (например, требует либерального использования оптимизаций shouldComponentUpdate() ... которые ломаются, если вы наивно "пересылаете" метод отправки внутри render() .)

В какой-то момент мне кажется, что я просто использую redurs + setState и заканчиваю: D Если / пока React не выяснит историю для экстернализации дерева состояний ... хотя, возможно, это то, что мы действительно обсуждаем

Я думаю, это то, что @threepointone пытается решить с помощью https://github.com/threepointone/redux-react-local

@acdlite концептуальная модель Elm отлично работает для локального состояния, но на самом деле это похоже на боль для использования в реальных приложениях, где мы должны использовать библиотеки, уделять внимание монтированию, обрабатывать эффекты параллакса или что-то еще ... См. https: // github.com/evancz/elm-architecture-tutorial/issues/49

@slorber Да, я согласен. Разве это не то, что я только что сказал? : D

Полагаю, ваши опасения немного другие. _Если_ (большое «если») вы должны были использовать архитектуру Elm в React, я не думаю, что у вас возникнут проблемы, о которых вы упомянули, потому что у нас все еще будет доступ к жизненным циклам, setState в качестве аварийного выхода и т. Д.

Да, мы на одной странице @acdlite
Архитектура Elm с React решила бы эту проблему.
Даже Elm может в будущем, поскольку он может позволить использовать крючки vdom.

Однако архитектура Elm требует некоторого шаблона. Я думаю, что это не совсем устойчиво, если вы не используете Flow или Typescript для упаковки / развертывания действий, и было бы лучше иметь более простой способ обработки этого локального состояния, например решение @threepointone

У меня есть еще одна проблема, связанная с архитектурой Elm, заключается в том, что вложенное действие отлично работает для состояния локального компонента, но я думаю, что это не очень хорошая идея, если вам нужна связь между разделенными компонентами. Если widget1 необходимо развернуть и проанализировать глубоко вложенные действия, чтобы найти какое-либо событие, инициированное widget2, возникает проблема связывания. Я высказал здесь некоторые мысли и думаю, что использование Elm с двумя "почтовыми ящиками" может сделать эту работу. Это также то, в чем redux-saga может помочь в архитектурах без вяза с плоскими событиями.

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

@gaearon Вы говорите о react-redux-provide в своем последнем посте? Я спрашиваю, потому что если да, то, судя по вашему ответу, ясно, что вы недостаточно внимательно рассмотрели это.

Кроме того, что касается упоминания @acdlite об экстернализации дерева состояний , это именно та проблема, которую поставщики предназначены для решения.

API, предоставляемый декоратором provide безусловно, является абстракцией, и да, в настоящее время он зависит от redux , но думать об этом как о зависимости от redux - неправильный способ подумайте об этом, поскольку на самом деле это просто некий «клей» между компонентами и деревом состояний, поскольку context React еще не полностью разработан. Думайте об этом как об идеальном методе манипулирования и представления дерева (-ов) состояний через actions и reducers (то есть состояние магазина) как props , где вы можете по существу объявить actions и reducers как propTypes (и / или contextTypes в будущем) аналогично тому, как вы import что-нибудь еще в вашем приложении. Когда вы так делаете, все становится безумно легко рассуждать. context React потенциально может развиваться, чтобы включать эти базовые функции, и в этом случае потребуется целых 2 секунды, чтобы grep и удалить @provide и import provide from 'react-redux-provide' чтобы вы затем остались простые компоненты, которые больше от него не зависят. А если этого никогда не произойдет, ничего страшного, потому что все будет работать идеально и предсказуемо.

Я думаю, вы слишком упрощаете проблему. Вам еще нужно решить монтаж и демонтаж.

@acdlite Есть пример?

Если одна из вещей, о которой вы говорите, это (например) получение props из некоторой базы данных на основе некоторого id или чего-то еще, это (среди почти любого другого практического приложения, о котором вы можете подумать) может быть легко достигнута с поставщиками. Когда я закончу еще кое-что, над чем работаю, я буду рад показать вам, насколько это просто. :)

Может быть интересно: сравнение реализаций реактивного состояния .
Elm, Redux, CycleJS ... (в разработке)

Другая возможная потребность в нескольких магазинах возникает, когда возможны и необходимы несколько _временных линий_, _время путешествия_, сохранения местоположений и частоты сохранения. Например, user data и user actions которые относятся к одному пользователю, а common data и group actions относятся к группе пользователей (например, общий многопользовательский проект или документ). Это упрощает отделение того, что можно отменить (изменения личных данных), от того, что исправлено (изменения, уже внесенные другими пользователями), а также упрощает использование различных циклов сохранения и / или синхронизации.

Мы находимся на этапе проектирования / ранней разработки приложения angular 1.x с мышлением разработчиков angular 2.0 и думаем об использовании redux. В прошлом мы использовали поток с несколькими магазинами, отвечающими на некоторые общие рассылки и на некоторые специальные рассылки.

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

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

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

@VivekPMenon : что вас беспокоит? Размер дерева состояний? Возможность выполнять действия в пространстве имен / изолировать редукторы? Возможность динамически добавлять или удалять редукторы?

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

@markerikson. Большое спасибо за ответ. Мое замешательство в основном связано со второй точкой (изоляция). Предположим, одна из наших команд создает виджет A, основанный на потоке, и которому необходимо использовать хранилище для управления своим состоянием. Предположим, они создадут пакет jspm / npm и распространят его. То же самое и с другой командой, которая создает виджет B. Это независимые виджеты / пакеты, которым не нужно иметь прямую / косвенную зависимость друг от друга.

В этом контексте в мире регулярных потоков. Виджеты A и B имеют свои собственные хранилища внутри своего пакета (они могут прослушивать некоторые общие действия, но предполагают, что у них есть другие действия, которые не являются общими). И оба виджета будут зависеть от глобального диспетчера. В шаблоне redux, где это хранилище будет создано (физически). Компонент пользовательского интерфейса виджета A и B должен использовать экземпляр хранилища для прослушивания изменений и получения состояния. В обычном потоке они работают с собственным экземпляром магазина. Я пытаюсь представить себе, как концепция единого магазина будет играть в этой ситуации.

@VivekPMenon, это правильный вопрос, который некоторые из нас пытаются решить.

Вы можете получить некоторую информацию здесь: https://github.com/slorber/scalable-frontend-with-elm-or-redux

Также посмотрите мой ответ о Redux-saga здесь: http://stackoverflow.com/a/34623840/82609

@slorber Большое спасибо за руководство. С нетерпением жду идей, которые появятся здесь https://github.com/slorber/scalable-frontend-with-elm-or-redux

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

@slorber

Позвольте мне начать с того, что сага о redux - замечательный проект, в который вложено очень много мыслей, и это никоим образом не умаляет этого.

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

Саги без гражданства

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

  • Вы не можете генераторы путешествий во времени
  • Вы не можете проследить путь, который он привел к текущему состоянию саги
  • Вы не можете определить начальное состояние
  • Вы не можете отделить бизнес-логику от состояния приложения [*]
  • Вы не можете хранить саги

[*] Это можно интерпретировать как определение бизнес-логики в определении модели приложения, что делает M и C в MVC одним и тем же, что можно рассматривать как антипаттерн.

Действия диспетчеризации не являются детерминированными

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

Сериализуемое состояние

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


Хранить логику в состоянии - это плохо, и, на мой взгляд, это то, что в некотором роде делает redux saga.

Совершенно нормально предположить, что состояние вашего приложения специфично для _your_ приложения. Это означает, что также нормально предполагать, что состояние приложения предсказуемо, пока оно работает поверх _ вашего_ приложения. Это означает, что можно хранить информацию о хранилище, которое определяет поведение вашего приложения, без фактического сохранения там логики.

@eloytoro

Я понимаю ваше беспокойство. Если вы посмотрите на проблемы redux-saga, вы увидите, что эти вещи обсуждаются и есть решения, которые нужно найти.

Также обратите внимание, что redux-saga выбирают для реализации с генераторами, но это вовсе не является требованием для реализации шаблона саги, и есть плюсы и минусы.

Существует эффект выбора, который позволяет вам использовать состояние redux внутри вашей redux-saga, поэтому, если вы не хотите скрывать состояние внутри генераторов, вы можете просто поместить состояние вашей саги в redux-store напрямую, но это требует больше работы.

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

Есть возможные решения для путешествий во времени, но пока нет конкретной реализации

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

При сравнении я помню несколько вещей:

  1. единственный источник истины
  2. насколько легко / сложно рендерить на стороне сервера и передавать исходные данные на стороне клиента
  3. инструменты разработчика, такие как отмена / повтор и постоянное хранение истории после перезагрузки.
  4. анимации, анимации, анимации. Я всегда спрашиваю себя _ "как анимация будет работать с решением a или b" _. Это ключевой момент для меня, поскольку продукты, которые я разрабатываю, ориентированы на UX. Я разделяю большую часть недоразумений @jsonnull .
  5. повторное использование компонентов внутри другого приложения
  6. гидратация ссылочных данных (_ основная проблема_) для сохранения целостности (связана с подачей исходных данных со стороны сервера или любого другого источника данных)
  7. сообщество других разработчиков для обсуждения, подобного этому

Так что это моменты, которые я опасаюсь, когда думаю о государственном управлении. Redux - победитель, отметив большинство полей. Я довольно хорошо знаю Redux, и это было первое, что заставило меня спокойно дышать в области клиентских js.

Наиболее заметными другими проектами, которые я изучал, являются шаблон SAM (_ трудно понять мышление и никаких инструментов_) и Mobx (_ известен своим изменяемым хранилищем_).

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

Мое (ограниченное, конечно) исследование Mobx привело к следующему:

  1. Mobx (как и Redux) выступает за единый источник истины, но как класс с наблюдаемыми свойствами. Эта концепция создает различия с редукцией и потоком в целом:

    1. Mobx также выступает за субмагазины (_вложенные / часть единого глобального магазина_). Для меня это очень похоже на определение клиентской схемы. Это немного раздражает, так как у вас, вероятно, тоже есть схема на сервере. Я вижу сходство с redux-orm . AFAIK, суб-хранилища предназначены для передачи компонентам в качестве реквизита (таким образом, компонент не полагается на состояние отображения в реквизиты или определенный ключ глобального дерева). Однако передача одного свойства нарушает мое представление о «свойствах» - они должны описывать то, что нужно компоненту. Это на самом деле заставляет меня задуматься, будет ли это проблемой, если React propTypes может использоваться для определения формы передаваемого хранилища. Также: наличие хранилища и суб-хранилищ в качестве классов позволяет напрямую ссылаться на данные без необходимости нормализации, но это бессмысленно, если вы хотите загружать исходные данные из-за:

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

  2. История и отмена / возврат могут быть достигнуты путем создания снимков состояния после его изменения. Но вам нужно самому написать сериализатор, десериализатор. Пример приведен здесь: https://github.com/mobxjs/mobx-reactive2015-demo/blob/master/src/stores/time.js
  3. Хорошая вещь в написании сериализаторов заключается в том, что вы можете опустить вещи. похоже, хорошее место для хранения непостоянных данных, связанных с анимацией. Поэтому, если вы сериализуете его, вы просто проигнорируете его, и у вас не будет повторения / отмены, полного шагов анимации.
  4. Mobx выступает за сохранение производных данных в хранилище (как методов хранилища) в отличие от декоратора reselect + connect . Мне это кажется неправильным.
  5. Благодаря своей наблюдаемой природе, mobx позволяет легко определять эффективные чистые компоненты. Поверхностное сравнение свойств не проблема, потому что содержимое в любом случае можно наблюдать.

Сравнивая SAM и Redux, мне нравится думать о логике:

  1. Redux:

    1. Определить магазин

    2. Определите функции, которые обрабатывают изменения в хранилище (редукторы)

    3. Определите идентификаторы событий (действие redux) (тип действия redux) для каждого изменения хранилища и запускайте конкретный редуктор, когда он соответствует

    4. Сопоставьте хранилище со свойствами контейнера компонентов, при необходимости передавая обработчики взаимодействия с пользователем (щелчок, тип, sscroll и т. Д.) В качестве функций, которые просто запускают действие события / отправки.

    5. Компонент запускает событие (действие редукции), которое сопоставляется со всеми редукторами

    6. Выполните повторную визуализацию с новым состоянием.

  2. СЭМ:

    1. Определите магазин.

    2. Определите функцию доступа / установки для магазина. Этот установщик принимает любые данные и исключительно на основе данных пытается выяснить, куда их поместить в хранилище и нужно ли запрашивать данные с сервера или сохранять их там. Выяснить, что делать с данными, не всегда можно, просто изучив их, поэтому иногда используются идентификаторы / флаги (например, isForgottenPassword ). Этот установщик также предназначен для проверки данных и хранения ошибок. После того, как сеттер закончил -> перерисовать с новым состоянием.

    3. Определите функции, которые принимают часть данных, преобразуют ее (альтернатива редукторам redux) и запускают установщик, передающий преобразованные данные. Эти функции теперь имеют представление о форме магазина. Они знают только те данные, которые им переданы.

    4. Сопоставьте данные хранилища со свойствами контейнера компонентов, но также обязательно передать обработчики обработчиков взаимодействия с пользователем (щелчок, ввод, прокрутка и т. Д.). Обратите внимание, что это фактические функции (известные как redux в redux), а не функции, которые выполняют вызов диспетчеризации.

    5. Примечание: нет диспетчеризации действий, прямой переход редукторов.

Итак, все redux, SAM и Mobx хранят состояние в одном месте. То, как они это делают, приносит свои собственные неудачи. Пока никто не может победить redux и его экосистему.

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

Я работаю над некоторыми концепциями, как лучше обрабатывать такое несериализуемое состояние. Включая воссоздание его из любого заданного состояния. Мне уже удалось переместить все, кроме ссылок на функции / объекты, обратно в магазин. Остались объекты с собственными жизненными циклами и определенным API, которые взаимодействуют с моим приложением и, следовательно, с моими действиями. - Похоже на компоненты React, только не для рендеринга пользовательского интерфейса, а для настраиваемой логики.

Я не знаю, читали ли вы эту статью Эрика Мейера . Просто цитата:

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

Возможно, пришло время вернуться ко всей этой ерунде о неизменности. Все скажут вам, что это снижает производительность, когда единое дерево состояний увеличивается в размерах. Шаблон SAM превращает мутацию в основной класс модели программирования, когда вы делаете это, происходит множество упрощений, от того, что вы больше не нуждаетесь в Sagas, до всех этих вспомогательных конечных автоматов, необходимых для отслеживания эффектов (например, выборки).

Статья не в вашу пользу. Эрик Мейер пересматривает «эту бессмыслицу о неизменности» и полностью ее принимает . Он утверждает, что удаление мутации состояний - это основной первый шаг , фундаментальный, но недостаточный. Его конечная цель - полное устранение неявных побочных эффектов, которые сделали бы любое управление состоянием устаревшим (не было бы состояния, которым можно было бы управлять!).

Я фанат работ Эрика Мейера, и с удовольствием буду читать и слушать все, что он скажет, помня при этом, что он не работает в тех же деловых, технологических и интеллектуальных условиях, что и я.

Ключевая проблема в разработке программного обеспечения состоит в том, что все, кажется, верят, что назначение - это мутация IS-A. Языки программирования поддерживают только присваивания, изменение остается на усмотрение программиста.

Добавление обертки функции к группе назначений не означает, что он автоматически станет лучшим способом изменения состояния. Это чепуха.

За (Application) State Mutation лежит теория, она называется TLA +, как ни странно ... Эрик никогда не упоминает TLA + ...

JJ-

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

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

В информатике существует практически один принцип, с которым мы все можем согласиться, - утверждает доктор Лэмпорт: «Большая часть информатики связана с конечными машинами». и продолжает: «Тем не менее, специалисты по информатике настолько сосредоточены на языках, которые используются для описания вычислений, что в значительной степени не знают, что все эти языки описывают машины состояний».

Действительно, все языки программирования позволяют нам определять «конечные автоматы» независимо от их семантики. К сожалению, многие языки программирования ориентированы на «действие», а не на «состояние-действие». Это не просто так: существует большой класс проблем, которые гораздо более эффективно кодируются с помощью формализма, основанного на действиях (где состояния конечного автомата можно в значительной степени игнорировать). Когда вы игнорируете их, вы, по сути, заявляете, что все действия возможны в любом данном состоянии, что, как мы все знаем, в целом неверно.

Вы можете сколько угодно смеяться над примером Эрика, ха-ха:
// prints Ha var ha = Ha(); var haha = ha+ha;

но все, что он утверждает, это то, что вы совершаете неправильные действия в данном состоянии. Ни больше ни меньше.

Что дает функциональное программирование? совсем не очень. Он очень запутанно выражает связь между действием и результатами, которые оно может иметь. Это немного похоже на попытку бросить стрелу (даже умную монадическую стрелу) в гигантский лабиринт и надеяться, что вы попадете в цель.

TLA + предлагает гораздо более естественный способ борьбы с эффектами, во-первых, потому что он дает четкое представление о том, что такое шаг и как мутация связана с действием. Вы можете игнорировать TLA +, но это немного похоже на то, что вы жалуетесь, что пытались отправить ракету на Луну, исходя из предположения, что Земля плоская, и управлять космическим кораблем было действительно сложно. Я знаю, что мы живем в мире, где люди верят, что могут изменить реальность, пока достаточное количество людей верят в то, что они говорят ... что это все равно ни к чему не приведет.

И снова, мне было бы очень, очень, очень любопытно узнать, что Эрик думает о TLA +, я связался с ним в LinkedIn и задал вопрос. Так и не получил ответа ... В конце концов, доктор Лэмпорт получил за это только премию Тьюринга, вероятно, это не стоит его времени.

Честно говоря, @jdubray , вы на грани троллинга. Вы повторяли свои аргументы десятки раз в этой беседе, в других вопросах и в других местах. Вы оскорбляли Дэна и сообщество Redux, вы размахивали своими учетными данными и продолжали ссылаться на слова «TLA +» и «Lamport», как будто они являются Святым Граалем и Ответом на жизнь, Вселенную и все остальное. Вы настаивали на том, что SAM намного превосходит Redux, но при этом не слишком много писали о значимых приложениях для сравнения, несмотря на многочисленные приглашения предоставить доказательства. Вы действительно не собираетесь изменить мнение никого, кто еще не убежден. Я честно предлагаю вам заняться чем-нибудь более продуктивным.

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

@markerikson Я хотел бы продолжить эту дискуссию, даже если она никуда не
Кроме того, @antitoxic (без каламбура) ясно дал понять, где подход, на который он ссылается, полезен или нет, мы знаем, насколько плохи другие проблемы из-за его вмешательства, поэтому нет смысла больше слушать

Ничего страшного Марк, можешь удалить мои комментарии, зачем портить такой блестящий разговор?

@jdubray, только вы видите это как блестящее. Я думаю, мы все чувствуем, что вы пытаетесь продать нам что-то без дополнительных аргументов, чем «Лэмпорт получил премию Тьюринга, так что послушайте меня, я прав!».

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

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

На самом деле вы всегда используете такие термины, как I believe everyone agrees on that во всех своих сообщениях. Это не что-то хорошее, полезное или гостеприимное для людей. Мы не все согласны с вами по умолчанию, и большинство из нас не читали и не будут читать статью Эрика. Если вы не можете убедить людей доброжелательно, не прося их заранее прочитать тонны бумаги и заставляя их чувствовать себя глупыми, если они не будут или не согласны с вами, это не поможет распространить ваши идеи.

Вы можете игнорировать TLA +, но это немного похоже на то, что вы жалуетесь, что пытались отправить ракету на Луну, исходя из предположения, что Земля плоская, и управлять космическим кораблем было действительно сложно.

Это однозначно выглядит как аналогия, а не как первый принцип . Серьезно, как вы думаете, можно ли рассматривать это заявление как «блестящую дискуссию»?

Я думаю, что обсуждения на самом деле становятся более яркими, когда вы не говорите в них, потому что вы монополизируете все внимание на TLA + и фактически не позволяете людям, не заинтересованным в TLA +, пытаться отправить свою ракету на Луну с чем-то более конкретным, чем в среднем. люди могут понять, как Redux.

Вы все еще не говорите, что TLA + того не стоит, просто вы говорите об этом неправильно, не тем людям. Может быть, если Эрик не ответил вам, это потому, что он тоже вас не понимает? Может быть, вы настолько умны, что вас может понять только Лэмпорт? Может быть, в будущем вы выиграете премию Тьюринга? Я не знаю, но одно можно сказать наверняка: текущая дискуссия ничего не дает.

Может быть, понравится @gaearon и создаст

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

  • Разделяйте саги так, чтобы в каждой из них было не более одного put . Если у вас есть саги с более чем put разделите их и объедините в цепочку с эффектом call . Например
function* mySaga() {
  yield put(action1());
  yield put(action2());
}

// split into

function* mySaga() {
  yield put(action1());
  yield call(myNextSaga);
}

function* myNextSaga() {
  yield put(action2());
}
  • Саги с эффектами take должны быть разделены на логику, которая следует за take и начальной точкой саги. Например
function* mySaga() {
  yield take(ACTION);
  // logic
}

// split it into

function* rootSaga() {
  yield takeLatest(ACTION, mySaga);
}

function* mySaga() {
  // logic
}
  • Для каждой саги создайте своего рода "контрольную точку", что означает, что при инициализации считывается из состояния, а оттуда call сага, которая вам нужна
function* rootSaga() {
  // emit all forks and take effects that need to run in the background
  yield call(recoverCheckpoint);
}

function* recoverCheckpoint() {
  const state = yield select();
  if (state.isFetching) {
    // run the saga that left the state like this
  }
}

Объяснение того, почему это сработает, основано на наборе предположений, которые в целом верны.

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

Такое разделение саг также имеет положительные _эффекты_

  • Саги более многоразовые
  • Саги легче изменить
  • Саги легче понять

но @slorber :

Многие люди находят этот образец «игры про рыбалку» весьма интересным для программирования, поскольку он из первых рук показывает, как SAM работает в целом, включая представление динамического состояния и «предикат следующего действия». NAP устраняет необходимость в Sagas и не требует нарушения принципа Redux №1, который представляет собой единое дерево состояний, но что я знаю?

Иногда, когда я использую Ableton Live, у меня в голове возникает мысль: «Можно ли написать графический интерфейс Ableton Live как приложение React / Redux?» Думаю, ответ - нет, просто было бы слишком сложно получить приемлемую производительность. Есть причина, по которой он написан на Qt, и причина того, что Qt имеет архитектуру сигналов / слотов.

Я использовал React / Redux с Meteor. Я храню документы mongo в состоянии Immutable.js, отправляя действия Redux для обратных вызовов в Mongo.Collection.observeChanges . Недавно я обнаружил, что при подписке на несколько тысяч документов пользовательский интерфейс был чрезвычайно медленным при запуске, потому что тысячи действий Redux отправлялись, когда Meteor отправлял исходные результаты подписки один за другим, что приводило к тысячам операций Immutable.js и тысячам повторных рендеров так же быстро насколько возможно. Я предполагаю, что RethinkDB также работает таким же образом, хотя я не уверен.

Мое решение заключалось в создании специального промежуточного программного обеспечения, которое бы откладывало эти действия по сбору и затем отправляло их партиями с заданной скоростью, чтобы я мог добавить ~ 800 документов за одно изменение состояния. Это решило проблемы с производительностью, но изменение порядка событий по своей сути рискованно, поскольку может привести к несогласованному состоянию. Например, мне нужно было убедиться, что действия по состоянию подписки отправлялись через один и тот же дроссель, чтобы подписки не были помечены как готовые до того, как все документы были добавлены в состояние Redux.

@ jedwards1211 У меня была аналогичная проблема (много маленьких сообщений, которые составляли исходные данные).

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

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

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

https://github.com/stephenbunch/redux-branch

@ jedwards1211 Я думаю, что по мере того, как приложения становятся все

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

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

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

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

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

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

Если бы я сегодня создавал сложное приложение, я бы начал с Redux-подобного класса магазина верхнего уровня, поддерживаемого несколькими разными магазинами. Примерно:

  • В хранилище данных верхнего уровня есть очередь для входящих действий
  • Действия предназначены либо для действий анимации, которые должны быть отправлены быстро, либо для взаимодействий приложений, которые имеют более низкий приоритет.
  • Действия анимации в очереди отправляются на наблюдаемый бэкэнд из цикла requestAnimationFrame.
  • Действия взаимодействия отправляются в redux, когда действия анимации из очереди завершены

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

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

@ jedwards1211 Я могу вспомнить пару случаев, когда состояние локальной анимации не работает. Я согласен, это не те случаи, с которыми вы столкнетесь с каждым приложением, но я думаю, что они возникают достаточно часто.

В одном случае вы используете библиотеку, отличную от Redux, где у вас нет локального состояния. (Хорошо, да, я знаю, что здесь я немного обманываю.) Если вы используете очень простой подход с гиперкриптом, без виртуального dom, чисто функциональные компоненты, то вместо локального состояния вам придется передать некоторую анимацию состояние корневого узла или любого другого узла, который вы повторно визуализируете.

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

Ключевым моментом здесь является то, что есть два способа сделать эту анимацию - сделать ее как часть вашего рендера и сохранить метафору «UI как функция состояния» нетронутой, или изменить DOM отдельно. Почти всегда лучший ответ - просто перерисовать, а не изменять.


Теперь рассмотрим пример, в котором у вас есть возможность сохранить локальное состояние некоторой анимации - создание браузерной игры с пользовательским интерфейсом на основе React поверх нее.

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

Пример: персонаж-спрайт имеет анимацию, состоящую из 10 кадров, и вы хотите воспроизвести анимацию на протяжении ~ 400 мс, поэтому вы собираетесь менять спрайт, отображаемый каждые 2 тика или около того.

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

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

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

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

Вообще говоря, также намного лучше координировать анимацию, передавая время запуска / остановки через Redux и позволяя компонентам поддерживать остальную часть состояния локально.

Это касается не только игр. Любая расширенная последовательность анимаций может пройти через это, например, если вы хотите создать серию длительных анимаций на сайте с помощью React.

@jsonnull круто, это отличный пример, спасибо, что поделились этим.

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

var user = {id: 'user1', posts: [], comments: []}
var post = {id: 'post1', user: user, comments: []}
user.posts.push(post);
var comment = {id: 'comment1', post: post, user: user}
post.coments.push(comment)
user.comments.push(comment)
appState.user = user

Redux не позволяет использовать иерархии объектов с циклическими ссылками. В Mobx (или Cellx) вы можете просто иметь ссылки один-ко-многим и многие-ко-многим с объектами, что во много раз упростило бизнес-логику.

@bgnorlov Я не думаю, что что-то в пакете redux запрещает циклические ссылки, о которых вы говорите - они просто затрудняют клонирование вашего state в вашем редукторе. Кроме того, может быть проще вносить изменения в состояние с помощью циклических ссылок, если вы используете Immutable.js для представления своего состояния, хотя я не уверен.

Вы также можете моделировать отношения в своем состоянии redux, используя внешние ключи, хотя я понимаю, что это не так удобно, как использование ORM-подобных ссылок на объекты:

var user = {id: 'user1', postIds: [], commentIds: []}
var post = {id: 'post1', userId: user.id, commentIds: []}
user.postIds.push(post.id);
var comment = {id: 'comment1', postId: post.id, userId: user.id}
post.commentIds.push(comment.id)
user.commentIds.push(comment.id)
appState.userId = user.id
appState.posts = {[post.id]: post}
appState.comments = {[comment.id]: comment}

// then join things like so:
var postsWithComments = _.map(appState.posts, post => ({
  ...post,
  comments: post.commentIds.map(id => appState.comments[id]),
})

@ jedwards1211 для клонирования состояния с циклическими ссылками в redux, reducer должен возвращать каждую новую копию объекта, затронутого изменениями. Если ссылка на объект изменяется, связанный объект также должен быть новой копией, и это будет повторяться рекурсивно, и оно будет генерировать совершенно новое состояние при каждом изменении. Immutable.js не может хранить циклические ссылки.
При подходе к нормализации, когда некоторому обработчику событий требуются некоторые данные, ему каждый раз требуется извлекать объект по его идентификатору из глобального состояния. Например - мне нужно отфильтровать братьев и сестер задач где-то в обработчике событий (задачи могут иметь иерархическую структуру). С redux мне нужно отправить thunk, чтобы получить доступ к состоянию приложения.

var prevTask = dispatch((_, getState)=>getState().tables.tasks[task.parentId]).children.map(childId=>dispatch((_, getState)=>getState().tables.tasks[childId])).filter(task=>...) [0]

или с селекторами

var prevTask = dispatch(getTaskById(task.parentId)).children.map(childId=>dispatch(getTaskById(childId)).filter(task=>...)[0]

и этот шаблон превращает код в беспорядок по сравнению с версией mobx, когда я могу просто написать

var prevTask = task.parent.children.filter(task=>...)[0]

@ jedwards1211 , @bgnorlov : FWIW, это одна из причин, почему мне нравится Redux-ORM . Это позволяет вам поддерживать ваше хранилище Redux в нормализованном состоянии, но упрощает выполнение этих реляционных обновлений и поиска. Фактически, последний пример в основном идентичен Redux-ORM.

Я только что написал пару сообщений в блоге, описывающих основы Redux-ORM , а также основные концепции и расширенное использование .

@markerikson круто, спасибо за подсказку!

@gaearon

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

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

Исходя из угловатого и явно не связанного с потоком / редукцией фона, очень сложно сформулировать эту идею самостоятельно. Особенно, когда множество примеров по-прежнему создают действия для каждой клавиши в текстовом поле. Я бы хотел, чтобы кто-нибудь поместил эту цитату в текст размером 50 пикселей вверху каждой страницы документации Redux.

@leff Согласен. Некоторое время назад я синтезировал эту точную идею, но ее недостаточно. Даже если вы используете Redux для управления историей, часто бывает много эфемерных состояний, которые не должны обременять ваше управление состоянием в стиле Redux.

Это не обязательно будет сюрпризом для людей, у которых была возможность поработать, например, над зрелым настольным приложением, которое выполняет обширные функции отмены / повтора. Но для океанов новичков, пришедших к этим идеям через Redux, это будет откровением.

@bgnorlov , это хорошо, что невозможно хранить циклические ссылки с Immutable.js, я никогда об этом не думал! Я думаю, что это хорошая особенность.

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

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

FWIW, документы указывают на то, что не все должно переходить в Redux, согласно http://redux.js.org/docs/faq/OrganizingState.html#organizing -state-only-redux-state.

В последнее время в сети было довольно много разговоров о том, для чего «подходит» Redux. Некоторые люди видят преимущества размещения буквально всего в Redux, другие считают, что это слишком хлопотно, и хотят хранить только данные, полученные с сервера. Итак, здесь определенно нет фиксированного правила.

Как всегда, если у людей есть идеи по улучшению документации, мы приветствуем PR :)

Если вам когда-нибудь понадобится повторно использовать редукторы и у вас будет возможность выделять "подсостояния" в вашем состоянии редукции, я разработал плагин, чтобы сделать это легко (с интеграцией React)

https://github.com/eloytoro/react-redux-uuid

@eloytoro IMO, если у вас есть проблемы с каким-либо сторонним редуктором, имеющим жестко заданные типы действий или местоположение в государстве, вы должны открыть проблему в проекте ... в ближайшее время это станет неприемлемой практикой проектирования, поскольку люди изучают повторно используемый редуктор / действие создатель шаблона.

@eloytoro Я собирался написать что-то подобное. Большое спасибо за ссылку!

@ jedwards1211 Думаю, вы ошиблись. Я никоим образом не упоминал сторонние редукторы или проблемы с ними, просто пытаюсь продемонстрировать, как мой фрагмент может решить проблему с повторным использованием редукторов в коллекциях с динамическими размерами.

@avesus надеюсь, что это

@eloytoro а, в этом больше смысла.

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

Единое государство всегда лучше, однако не переусердствуйте. Получение каждого keyDown () через магазин и обратно в представление просто сбивает с толку и не нужно. Таким образом, переходные состояния должны обрабатываться состояниями локальных компонентов (например, React).

Я думаю, что, поскольку сокращение и реакция воспламенили наблюдаемую сеть, количество хорошей анимации на страницах уменьшилось.

@ mib32 , возможно, ты прав! Надеюсь, люди со временем привыкнут к созданию хорошей анимации в React.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги