marshinov
0
Ну да сага почище чуток. Но в итоге то один фиг — в майдлвейр попадает action / thunk, который приведет к запросу на сервер. Если контейнеры и компоненты разделены, то для целевого презентационного компонента вызов будет выглядеть одинаково this.props.fetch({...}. И вот тут уже возникает вопрос как обработать континюейшн по завершению fetch'а. Сага дает возможность написать takeEvery / takeLatest. Я себе для загрузки данных написал вот такую штуку:

const query = (moduleRef, url, params = undefined) => {
    if(typeof (url) == 'object'){
      params = url
      url = DATA
    }

    dispatch({
      type: combinePath(moduleRef, GET),
      params
    })

    return new Promise(resolve => {
      dispatch(function () {
        get(url, params).then(response => {
          const error = 'ok' in response && !response.ok
          const data = error
            ? {ok: response.ok, status: response.status}
            : response

          dispatch({
            type: combinePath(moduleRef, GET + (error ? FAILED : SUCCEEDED)),
            payload: data
          })

          resolve(data)
        })
      })
    })

Сами компоненты не знают, что они вызывают и какие там континюейшны. Главное, что придет либо GetSucceeded либо GetFailed. На это уже реагируют редюсеры. Функцию можно чейнить, чтобы вызывать цепочку загрузок. Обошелся в итоге без саги и без ручных вызовов then. Понятно, что я обрабатываю только один сценарий асинхронности: загрузка данных с сервера по цепочке и падение при любой ошибке. Для моих задач пока подходит, нигде не уперся.

Да, в крайнем случае придется написать ручной then, но это все-равно проще, чем саги. Да и фиг его знает, когда генераторы будут поддерживаться всеми браузерами и не внесут ли изменений в стандарт.
marshinov
+1
Как вы предлагаете работать с состоянием в react? Выбор то не большой: redux и mobx.
marshinov
0
И тогда у вас два разных способа хранить это состояние, а следовательно изменение одного на другое затрагивает дополнительный слой, что можно было бы безболезненно избежать не будь редакс таким редаксом)

Ну так и нормально. Одно состояние персистентно, другое — нет. Для них разные хранилища. Все логично же, не?
marshinov
0
Свои данные можете добавить через замыкания. Объектами можно добавить индексы вот так:
route.indexRoute = {
        component: component
      }
marshinov
0
Тогда я храню ее в redux store и получаю с сервера. Разделение же очень простое — если после повторного рендера компонента на эту часть состояния наплевать — храним ее в локальном стейте компонента. Если нужно сохранять/получать из вне — храним в redux store.
marshinov
0
Отсюда и все эти thunk'и, promise'ы, react router'ы (честно говоря, вот это — вообще капец), и прочее.

  1. Чем вам не нравится react router?
  2. Как без thunk'а или redux-saga (или аналогичного middleware) работать с асинхронностью и другими эффектами, если reducer'ы — это чистые функции?


marshinov
0
Покажите, где в подходе автора стало лучше, чем в redux и более MVC? Я вижу знакомую картину, вид в профиль.
marshinov
+2
Ну на самом деле там и правда есть темные пятна на этом редаксе. Вот даже из того, что тут обсуждалось. Использовать setState — крайне не-редакс вей.

Разработчик redux с вами не согласен. Вообще, Абрамов во всех комментариях очень дельно пишет о cargo cult в JS. Все пытаются найти серебряную пуля и всегда использовать только ее. Разработка так не работает. Есть задачи, есть инструменты. В комментах выше уже написали, что для всевозможных флажков (эфемерное состояние) вполне себе подходит использование setState.

marshinov
0
Моя проблема не в swtich (он меня полностью устраивает), а в boilerplate в редюсерах.
marshinov
0
Скажите, а вы рассматриваете проект только в качестве лучшего понимания внутреннего устройства Redux или еще в каком-то виде?

Ваша архитектура идеально ложится на redux:

  1. ваши action'ы полностью аналогичны action'ам Redux. Константы можно не использовать
  2. dispatcher = передать функцию store.dispatch во все «законекченные компоненты» и подкючить thunk
  3. router = подключить react router
  4. HoC = компоненты/контейнеры — паттерн реакта
  5. unsubscribe реализуется через replaceReducer

Т.е. все, что вы хотите реализуется на существующем стеке. Не лучше ли инвестировать время на то, чтобы хорошо настроить существующие инструменты, чем создавать новые?

marshinov
+1
Или если вы хотите инициализировать компонент один раз (при первом рендере), а потом при повторном отображении компонента, например если пользователь ушел на другой экран и вернулся, показать состояние, которое пользователь создал до ухода с экрана.
marshinov
0
Мне тоже очень не понравился boilerplate в switch'ах. Смог избавить с помощью фабрик редюсеров.

const reducerFactory = (moduleRef, initialState, next = null, method = GET) => {
  if (!moduleRef) {
    throw Error('You must provide valid module ref')
  }

  if (!initialState) {
    throw Error('You must provide valid initialState')
  }

  return (state = initialState, action) => {
    if (!action.type.startsWith(moduleRef))return state

    if (action.type === combinePath(moduleRef, GET)) {
      return {...state, params: {...action.params}, [IS_FETCHING]: true}
    }

    if (action.type === combinePath(moduleRef, GET + SUCCEEDED)) {
      return {...state, ...action.payload, [IS_FETCHING]: false, [IS_INITIALIZED]: true}
    }

    if (action.type === combinePath(moduleRef, GET + FAILED)) {
      return {...state, ...action.payload, [IS_FETCHING]: false, [IS_INITIALIZED]: true}
    }

    if (action.type === combinePath(moduleRef, 'SetState')) {
      return action.newState
    }


    return typeof(next) == 'function' ? next(state, action) : state;
  }
}
marshinov
–1
Выше объявлены
const _children= Symbol('children')

Дописал в статью
marshinov
0
Может быть графика и вывод UI — это разные задачи? В gamedev'е отдельный поток на рендер — вроде как стандарт. Мне кажется, что рисование 100500 треугольников с помощью вращающихся кружков — вообще не кейс использования реакта
marshinov
0
Мне лично более симпатичен подход, когда в команде есть несколько спецов, которые пишут инструмент, а остальные его применяют. Но здесь требуется более тесное взаимодействие.

У нас «инструмент» — это ядро. Ядро пишут несколько человек. Остальная команда в основном использует. По примеру специалист еще не знакомый со стеком, может реализовать простой модуль не вникая в то, что под капотом. Более сложные модули требуют более высокой квалификации.

Есть вопрос по поводу redux: какой смысл использовать mapStateToProps? Умный контейнер прекрасно передает глупым компонентам свое состояние и каллбэки, setState вызывает цепочку рендеров. Реализация через редюсеры — это еще один уровень сложности. Зачем?
Я понимаю, когда нужно отправить команду на самый верх, чтобы она сверху вниз взбодрила кого надо, но в отношении родитель/дети redux лишний (имхо).

Эту часть не вполне понял :) Может привести пример?
marshinov
0
И как отличить, когда связанность высокая, а когда не очень, особенно если над проектом работает человек 50 из трех часовых поясов?)
marshinov
0
Я думаю, это уже очень специфичный для проекта вопрос. Может быть в вашем случае монолит — более удачное решение.
marshinov
0
А что мешает положить эту логику в App и использовать как мастер-пейдж для всего приложения? Или там гипотетические 100500 дешбордов в 27 конфигурациях?
marshinov
0
Сложно точно ответить без знания специфики вашего проекта. Расскажу как у нас. Любой подход, технология — работают в рамках каких-то ограничений. У нас большой объем разработки «учетных систем» и прочей «автоматизации бизнеса». Энтерпрайз короче. Самые часто используемые компоненты — это гриды, пагинации, деревья, формы. Т.е. инструменты манипуляции с данными. Все эти компоненты конфигурируются схемой данных, чтобы избежать неявного дублирования кода.

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

Если нужно сделать 2 примерно одинаковых модуля, но один — с перламутровыми пуговицами, используется техника copy/paste. Да, если захочется изменить оба модуля, то придется в два раза больше исправлять, но эта а) эту работу можно дать двум разработчикам, а не одному, б) если модули были чуть-чуть разными, то при модификации с большой вероятностью они разъедутся со всеми вытекающими из «хрупкого базового класса».

Чтобы ядро не «распухало» для UI нужно выбирать какие-то пакеты компонентов и ставить через npm.
marshinov
0
Возможно, выложу в open-source в среднесрочной перспективе. Пока нет достаточно времени для того, чтобы аккуратно все оформить. Вообще, приложенного кода достаточно, чтобы самостоятельно развернуть такую структуру, хотя придется конечно потратить время.
marshinov
0
  1. Полифилы для генераторов чего-то весят, сага чего-то весит, бандл получается увесистый
  2. Не все понимают генераторы. Крутая кривая обучения
  3. Компоновка саг — не тривиально

Т.е. больше из-за ограничений практического применения. С теоретической точки зрения мне саги больше нравятся.
marshinov
0
Пока четвертый роутер вообще использовать нельзя серьезно. С переходом можно легко «поправить» эту проблему. Так как все компоненты регистрируются в роутере в одном файле — routes.js, то просто оборачиваем целевой компонент в обертку с вызовом onEnter. Рефакторинга на 30 минут.
marshinov
+1
Ошибка при форматировании. Исправил.
marshinov
0
export const fetchReducerFactory = (name, initialState, callback) => (state = initialState, action) => {
  const keys = Object.keys(initialState)

  for(var i = 0; i < keys.length; i++){
    const actionType = name + '/' + toUpperCamelCase(keys[i])+ '/Fetch'

    if(action.type == actionType) {
      const res = {...state}
      res[keys[i]] = Object.assign({}, res[keys[i]], {...action, isFetching: true})
      if(typeof(res['ui']) == 'object' && keys[i] == 'data') {
        res['ui'] = Object.assign(res['ui'], action.params)
      }

      return res
    }

    const actionTypeSucceeded =
      (name + '/' + toUpperCamelCase(keys[i])+ '/FetchSucceeded').replace('Data/', '')

    if(action.type == actionTypeSucceeded) {
      const res = {...state}
      res[keys[i]] = Object.assign({}, res[keys[i]], {...action, isInitialized: true, isFetching: false})
      return res
    }

  }

  return typeof(callback) == 'function' ? callback(state, action) : state;
}


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

Используется так:
mdl.reducer = fetchReducerFactory(
  moduleId, {
    ...initialState,
    moduleId
 })
marshinov
0
Пример из реального приложения с сотнями моделей. Комбинируем редьюсеры так. Хочу услышать значительные замечания. Может быть что-то узнаю и исправлю.
marshinov
0
import Module1 from './modules/Module1'
import Module2 from './modules/Module2'

const combineModuleReducers = modules => {
  const reducers = {}

  for (let i in modules) {
    const red = modules[i].reducer
    if (typeof(red) !== 'function') {
      throw new Error('Module ' + i + ' does not define reducer!')
    }

    reducers[i] = red
  }

  return reducers
}

const modules = {
  Module1,
  Module2
}

const store = createAppStore(combineReducers(combineModuleReducers(modules)))

// код типового модуля
const mdl = {
   title: 'Мой модуль',
   reducer: (state = initialState) => {/* логика редюсера*/},
   path: '/module1'
}
marshinov
0
Вот так можно.
marshinov
0
Можно, главное ссылки менять. Просто {...state} не удобно будет делать. А без этого не поменяется ссылка и Redux не запустит перерендер компонентов. Вам просто не нравится, что Redux функциональный.
marshinov
0
Мне действительно все это требуется на фронте. Стек на беке я тоже написал среднестатистический. Очереди не везде конечно, а остальное у нас везде есть в том ли ином виде.
marshinov
0
Вообще-то можно. Для этого как раз и используется combineReducers. Это вопрос компоновки приложения, а не redux'а.
marshinov
–1
А что именно не нравится в редаксе? И в чем принципиальное преимущество mobx? Я когда сравнивал просто не нашел киллер-фич каких-то.
marshinov
0
Мы научили фронтов верстать сразу в реакт-компоненты. Это избавляет от этапа интеграции с серверной шаблонизацией, например. Можно отдельно делать фронт, отдельно бек и не разворачивать у фронта бд и все что там еще нужно. Большие возможности интерактивного UI. Думаю, это основные плюсы.
marshinov
+1
Action'ы redux'а — это же не доменные объекты, а скорее просто message. Что мешает ловить экшны, создающие эффекты в middleware, обрабатывать в любом удобном виде, в т.ч. с применением DDD и по завершению операции выбрасывать DomainEventSucceeded / Failed и уже их обрабатывать в редюсерах?
marshinov
0
Почему MVC? Почему фреймворк? Почему ORM? И т.д. Это вовсе не необходимые компоненты бэкенда. Я бы назвал их «модными», но бэкенд может быть построен на совершенно других принципах. Кмк, такое сравнение тут не совсем к месту.

Потому что примерно 90% кода из мира веб-разработки, который я видел или поддерживал — это MVC-фрейворк + ORM и не более. На PHP — это чаще всего Active Record, на Java — Hibernate, на .NET — Entity Framework. Да, бывают другие стеки. Но мейнстрим — именно ORM + MVC = love
marshinov
+2
Разве эта библиотека — не построена поверх react-router? Мне казалось, что она просто дополнительно пропускает переходы через store, чтобы сохранить time travel. Нет?
marshinov
0
Вспомнил, почему сделали так. У нас на IHasId<T> висит where T:IEquatable<T>. Так что привести к object нельзя, потому что object не реализует IEquatable. Убирать это условие не хочется, потому что тип ключа по определению должен быть сравним (чтобы иметь возможность быть уникальным). Интерфейс без T остался для поддержки композитных ключей.
marshinov
0
Спасибо за совет! Нужно проверить, что LINQ нормально отработает с ковариацией и выбросить без generic'а, если все ок.
marshinov
0
Чтобы можно было сделать каст к IHasId без generic'а. Бывают случаи, когда тип T не доступен.
marshinov
0
А чем это лучше использования task.IsFaulted?
<оффтоп>У вас на работе трейдинг что-ли (трейд эрроры, котировки)?:)</оффтоп>