Website development
JavaScript
Programming
ReactJS
Comments 50
+1

Оффтоп:
бщая концепция Flux кажется довольно привлекательной, но, попробовав react-redux, я остался недоволен — куча boilerplate-кода, вызов событий действий через switch по строкам??? Нет, это явно не лучший вариант, имхо

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;
  }
}
+3

Присмотритесь: вы заменили switch на цепочку if-ов. Более красивое решение предлагает, например, https://github.com/acdlite/redux-actions:


const increment = createAction('INCREMENT');
const decrement = createAction('DECREMENT');

const reducer = handleActions({
  [increment]: (state, action) => ({
    counter: state.counter + action.payload
  }),

  [decrement]: (state, action) => ({
    counter: state.counter - action.payload
  })
});
0

А такое решение как вам?


// utils.js
export default function createReducer(initState, handlers){
    return (state = initState, action = {}) => {
        if (handlers[action.type]){
            return handlers[action.type](state, action);
        }
        return state;
    };
}

// xx/actions.js
export const types = {
    LOAD: Symbol('LOAD'),
    SOME_ACTION: Symbol('SOME_ACTION'),
};
export default {

    load: () => {
        return {
            type: types.LOAD, 
            data: []
        }
    },

    someAction: () => {
        return {
            type: types.SOME_ACTION
        }
    }
};

// xx/reducer.js
import {createReducer} from '../../utils';
import {types} from './actions';

export default createReducer(initState, {

    [types.LOAD]: (state, action) => {
        return {
            ...state,
            list: action.data
        };
    },

    [types.SOME_ACTION]: (state, action) => {
      return {...state };
    }
}
0

Да практически то же самое, только в DevTools придётся типы экшенов как Symbol(LOAD), Symbol(SOME_ACTION) видеть.

+2
Символы в качестве экшен-тайпов лучше не использовать, так как они не сериализуются. (ну, если это вам нужно, конечно)
0
Моя проблема не в swtich (он меня полностью устраивает), а в boilerplate в редюсерах.
+4
Обращу внимание читателя, что обещание не выбрасывается наружу

Вот тут не соглашусь. Я нашёл для себя очень удобным, когда вызов action'а возвращает Promise, и я через .then в компоненте меняю локальный state компонента вещами, которые относятся только к компоненту (например ставлю флажок inProgress по которому блокирую кнопку, которую нажал пользователь). Потому что считаю неприемлимым загрязнять глобальный store сотнями булевых флажков, задача каждого из которых — заблокировать всего одну кнопку. А кнопок много, очень.

+1
Поддержу. Тоже так делаю и это очень удобно.

А флажки в глобальном store нужны только когда этот isProgress может влиять на несколько независимых компонентов.
+1
Или если вы хотите инициализировать компонент один раз (при первом рендере), а потом при повторном отображении компонента, например если пользователь ушел на другой экран и вернулся, показать состояние, которое пользователь создал до ухода с экрана.
-1

Только аккуратнее с такими фичами — иногда сброс состояния является ожидаемым поведением :-)


Например, все в том же примере с нажатой кнопкой.

-1

Если я правильно понял о чём речь, то ещё возврат Promise в actionCreator-е удобен в тестах.

0
Если вы реагируете на асинхронные действия и в then и в редукторах, тогда это уже не однонаправленный поток данных (Unidirectional Data Flow). Это уже что-то другое, будет два вектора направления данных.

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


Значения подобных флажков вычисляется в компоненте, методах жизненного цикла, на основе полученного состояния (сравнивая prevProps и nextProps).
+1
Более того, если на процесс загрузки данных компонента остальное приложение никак не реагирует, то не нужно выносить его (этого процесса) результаты в стор. Это все вместе прекрасно уживается в контейнере — и статус процесса (тот самый флаг), и его результат (ошибка или данные).
0
Проходили такое. Удобно только до тех пор, пока во время выполнения запроса компонент не может быть unmounted (например, переходом пользователя на другой экран приложения), а потом начинаются ошибки:
setState(...): Can only update a mounted or mounting component
0
Если данные перестали быть нужны в данный момент, это не значит, что данные не могут понадобится позднее — тут уже все зависит от конкретной ситуации.
В любом случае, отменять запрос в componentWillUnmount — это дополнительный код, в котором может быть ошибка и про который можно забыть. И как уже отмечали выше, изменение состояния компоненты через .then — нарушение принципа однонаправленного data flow.
+2
Если данные перестали быть нужны в данный момент, это не значит, что данные не могут понадобится позднее — тут уже все зависит от конкретной ситуации.

Ну это вы глупость сказали, мне аж неудобно

1. Ну вот когда они понадобятся, тогда их и загрузите
2. Ведь все-равно грузить будете — вы ведь не пишете код, что если данные уже загружены, то их не надо грузить
3. А если пишете, то как инвалидируете кеш? Как знаете, что загруженные когда-то данные — актуальные?
4. А если так волнуетесь, что данные могут понадобится позднее — почему бы заранее не грузить все данные которые могут понадобится позднее? Почему вы больший приоритет отдаёте тем данным, которые пользователь решил не загружать, а не каким-либо другим?
5. И вообще — могут понадобится, а могут и не понадобится, так зачем грузить, если можно не грузить?
6. Из-за глючного запроса, который по какой-то причине сейчас висит и который точно не нужен сейчас пользователю вы хотите занять место в канале и лимите ajax-запросов и тем самым оттянуть загрузку страницы, которая пользователю действительно нужна?

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

Так может необходимо иметь хорошую библиотеку, которая это делает сама, и в которой код протестирован? Тогда нет ни дополнительного кода, ни ошибки)

У вас просто Стокгольмский синдром. Раз из-за кривизны архитектуры редакса это не реализуется — значит не нужно? В редаксе вообще 75% всей модели — это дополнительный и ненужный код, который пишется только из-за необходимости копи-пасты, в котором точно есть ошибки и про которые вы забыли. Но насильник — хороший, о вас заботится и его надо защищать любой ценой.

И как уже отмечали выше, изменение состояния компоненты через .then — нарушение принципа однонаправленного data flow.

Да, я сперва тоже так подумал и хотел было отнести это к очередному минусу Редакс, но потом понял, что это просто заблуждение, потому написал так:

Использовать setState — крайне не-редакс вей. То есть, по сути, не вынес значение флажка в глобальный стор — нарушил философию редакса

Аргументирую. Подписка на then — нарушение принципов Редакса, и даже, возможно, Флакса, но не принципа однонаправленного флоу, т.к. флоу вполне однонаправленный, хоть и идет по малому кругу. Вот он:

1. Вы пишете в стейт (это наше хранилище)
2. Вы рендерите данные на базе данных стейта (реакт сам посылает событие „Change“ при их изменении)
3. При нажатии на кнопку мы создаем действие
4. Результатами действия становится внесение изменений в хранилище (а не внесение изменений в готовый рендер, как происходит в двонаправленном флоу).

Видите? [1 ⇒ 2 ⇒ 3 ⇒ 4 ⇒ 1]
Просто поклонение Редаксу затуманивает разум и мешает мыслить критически, как результат — все, что не соответствует Редаксу — не комильфо.
+1

Опять соглашусь.
В моём случае результат promise'а обрабатывается И в редюсерах (через dispatch в action'е) И в then в компоненте. Редюсеры меняют store ответом от сервера, then в компоненте меняет внутренний флажок, который разблокирует кнопку. Поток данных остаётся однонаправленным, но разветвляется по дороге.
Компонент при этом по максимуму остаётся переиспользуемым чёрным ящиком: его не надо завязывать ни на какой флажок в store вдобавок к action'у, который он должен дёрнуть (т.е. в props ему передаём только одну вещь — action). Своё внутреннее состояние он прячет в себе.

-5
Хорошая идея! Но сейчас на Хабре шапками закидают, так как вы «ноунейм».
0
Скажите, а вы рассматриваете проект только в качестве лучшего понимания внутреннего устройства Redux или еще в каком-то виде?

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

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

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

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

Первый пункт — написать redux самостоятельно, чтобы разобраться, как он работает. Второй пункт — убрать избыточность, но что-то непонятно, где именно вы её убрали. Больше похоже, что вы просто заменили функциональный подход на объектный. Третий пункт — NIH.

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

Редакс — просто не очень удачная реализация MVC. Так почему бы не пользоваться нормальными подходами вместо слабых подделок?
+1

Redux — это самый обыкновенный state machine. Мы посылаем сообщение машине состояний, и она это состояние меняет. Всё! Остальное — это маркетинговая шелуха из Фейсбука, пришедшая из их Флакса.

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

Ну вот к примеру почему вы аналогично про понимание не отреагировали на это?
Ваша архитектура идеально ложится на redux:

3. router = подключить react router

Потому и сказал, что Редакс — поделка в стиле MVC. Но я понимаю, что буквально — это не так.
0
А, теперь понятно :)

Ну, тут долго можно рассуждать на тему состыковки идеологий. К моему величайшему сожалению, мало кто может внятно описать понимание redux. Увы и ах. Отсюда и все эти thunk'и, promise'ы, react router'ы (честно говоря, вот это — вообще капец), и прочее. Видимо, действительно, не хватает дельной литературы, разжевывающей что к чему.
+3
Ну на самом деле там и правда есть темные пятна на этом редаксе. Вот даже из того, что тут обсуждалось. Использовать setState — крайне не-редакс вей. То есть, по сути, не вынес значение флажка в глобальный стор — нарушил философию редакса, пишешь грязные компоненты и на ближайшем афтер-пати тебя за это обольют смузи.

Но вот как написать универсальную библиотеку для редакса? Тот же аккордеон. Да, можно хранить все значения в стейте, но тогда зачем редакс? Я вот посмотрел на redux-accordion — и это тихий ужас. Иерархия просто отсутствует. Вот у нас есть какой-либо список и вместо того, чтобы данные аккордеона лежали там, рядом со списком — есть отдельное дерево для всех аккордеонов нашего приложения. Я знаю, что свидетели прихода святого Редакса скажут, что это круто.

И как добавить в эту деревянную структуру новые изменения? Вот если мне необходим статус «disabled» для листьев аккордеона? Я рядом создаю новое дерево с теми же ключами в виде «uniqId», где значения будут массив дизейбленых значений.

То есть вместо более логичной структуры

music: {
 data: { ... },
 accordionStatus: {
   0: { disabled: true, opened: false },
   1: { disabled: false, opened: true }
 },
}
videos: {
  data: { ... },
  Accordion: {
    0: { disabled: false, opened: false },
    1: { disabled: false, opened: true }
  }
}


Я получаю типичный говнокод в нашем стейте:

data: {
 music: {...}
 video: {...}
}
accordion: {
  music: { 0: false, 1: true },
  videos: { 0: true, 1: false } 
},
accordionDisables: {
  music: { 0: true, 1: false },
  videos: { 0: false, 1: false } 
},


Что за дикое говнище? А потом на это все еще подписываться нужно.

А я уж молчу, что для accordion и accordionDisables нам необходимо приблизительно одинаковые редюсеры, но вменяемого способа для реюза подобного кода в редаксе нету — придется

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

Looking a few years back, one of my biggest mistakes was my obsession with not repeating myself.
DRY is not free. You pay with a deeper abstraction stack and a higher price for changing individual cases later. I love boring code now.
Good boilerplate code makes modules disposable.


Я это все к чему
Видимо, действительно, не хватает дельной литературы, разжевывающей что к чему.

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

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

0
А если открытость-закрытость должна сохраняться на сервере и получатся с сервера?
0
Тогда я храню ее в redux store и получаю с сервера. Разделение же очень простое — если после повторного рендера компонента на эту часть состояния наплевать — храним ее в локальном стейте компонента. Если нужно сохранять/получать из вне — храним в redux store.
0
И тогда у вас два разных способа хранить это состояние, а следовательно изменение одного на другое затрагивает дополнительный слой, что можно было бы безболезненно избежать не будь редакс таким редаксом)

Ну и все-равно никуда не делись те проблемы, которые я описал в большом комментарии.

И кстати, на счет «Разработчик redux с вами не согласен». Все туда же:
мне кажется, что автор сам в душе понятия не имеет, как на редаксе писать не через жопу, потому приходится делать морду кирпичем и писать такие глупые и абсурдные вещи

0

https://github.com/reactjs/redux/issues/1385#issuecomment-187727168


My biggest pet peeve is it is harder to compose, reuse, nest, and generally move around container components because there are two independent hierarchies at the same time (views and reducers). It is also not entirely clear how to write reusable components that either use Redux as implementation detail or want to provide Redux-friendly interface. (There are different approaches.) I’m also not impressed about every action having to go “all the way” upwards instead of short-circuiting somewhere. In other words, I would like to see something like React local state model but backed by reducers, and I would like it to be highly practical and tuned to real use cases rather than a beautiful abstraction.
0
И тогда у вас два разных способа хранить это состояние, а следовательно изменение одного на другое затрагивает дополнительный слой, что можно было бы безболезненно избежать не будь редакс таким редаксом)

Ну так и нормально. Одно состояние персистентно, другое — нет. Для них разные хранилища. Все логично же, не?
+2

О, как вы хорошо описали мои ощущения от redux'а :-)


Всё пихать в глобальный store хорошо, пока приложение маленькое. А когда оно очень большое и долгоживущее (подразумевается, что открыто у пользователя весь рабочий день и активно используется), то уже надо писать специальные action'ы и логику в reducer'ах, единственная цель которых — очищать куски глобального стора, когда пользователь уходит со страницы/раздела/АРМ/придумайте-своё. Со state'ом компонента проще — как только он пропал из DOM, то все его данные пропали. Как снова появится в DOM — загрузим заново.

+1
Конечно, redux не имеет смысла использовать всегда и везде. Но что касается меня, по началу я пытался сделать все внутри самих компонентов, как это сделано в туториале реакта. Получилась просто ужасная каша вложенных друг в друга компонентов, которые передают друг другу обработчики и состояние. Когда дело дошло до того, чтобы отправить данные на сервер, мне пришлось решать эту проблему немного извращено: создавать скрытые инпуты, а потом сериализовать форму с помощью jQuery. Плохо, но на тот момент работало. Узнал про redux, изучил, нашел redux-actions, переписал все под него. Компоненты стали значительно проще, код более элегантным, читаемость намного выше. Остальные проблемы решились сами собой

Не вижу смысла писать все, как говорит redux. Если компонент используется в одном месте, коннекчу его прямо на месте. Если вдруг он понадобился ещё где-то — отделяю в отдельный файл. Проблем с переиспользованием редюсеров не было: если это набор компонентов, то в meta параметр можно запихать индекс или идентификатор объекта, а в map применить нужный редюсер к нужному элементу; или можно написать обёртку над экшеном для конкретного элемента для использования нескольких компонентов на странице.

У себя в проекте использую React сугубо для некоторых интерактивных вещей, необходимых только внутри самого сайта. Но пихать его всегда, везде и всюду или целиком делать сайт на нем… может это и имеет смысл, но не в моем случае.
+1

Делать сайт целиком на React (+ Redux, если надо) — точно не надо. Делать сложное и тяжёлое приложение целиком на фронтенд-фреймворке — точно надо (для сложных приложений ещё Ember себя хорошо показал, кстати говоря).

+3
А все потому, что аккордеоны — это не стейт приложения, и к стору они не имеют никакого отношения, и через redux проходить вообще не должны. Ну, а, если уж вам в каких-то абстрактных случаях нужно сделать их (аккордеоны) персистентными, то ничего криминального я не вижу в том, чтобы держать их состояния нормализованными.

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

Redux — это не готовое спасение от всех болезней, как активно продвигают авторы всех этих бесполезных путающих библиотек. Это идея, паттерн.
0
Отсюда и все эти thunk'и, promise'ы, react router'ы (честно говоря, вот это — вообще капец), и прочее.

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


0

Лично мне не нравится react-router своей дубовостью: я не могу добавить в роут свои данные, если писать роуты объектами, то нельзя добавить ни индексы, ни редиректы.

0
Свои данные можете добавить через замыкания. Объектами можно добавить индексы вот так:
route.indexRoute = {
        component: component
      }
+1
react-router (ну окей, версии до 4) достаточно плохо уживается с редаксом, так как тянет одеяло руления стейтом на себя — это раз. Далее, всеми правдами и неправдами поощряется складывать логику всевозможных редиректов и загрузку данных прямо в компонентах — например, любые вариации проверок на авторизацию — это чистой воды бизнес-логика, и мне хотелось бы видеть ее в подобающем месте, а не где-то внутри каких-то там guard'ов посередине дерева компонентов роутера или его конфига (о боги).

А вот вы зря thunk и саги в одну строчку, я как раз про последние хотел написать. Проблема в том, что thunk нарушает концепцию декларативности экшенов, так как инкапсулирует часть бизнес-логики там, где ее быть не должно.
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, но это все-равно проще, чем саги. Да и фиг его знает, когда генераторы будут поддерживаться всеми браузерами и не внесут ли изменений в стандарт.
+1
Само собой, все должно применяться по мере надобности. Если все что нужно, это дернуть апи и выгрузить данные для рендеринга какого-нибудь чартика, то не нужно для этого тащить саги. С другой стороны, не надо пытаться в thunk'ах уместить ту же обработку протухания сессии и редиректа на логин. Это банально проще и удобнее делать в саге.

А по поводу генераторов, не так там все плохо с поддержкой. Рано или поздно сафари допинают (даже уже), а IE наконец подохнет. Пока что, почти аналогичные процессы (отлько более уродливые) можно делать на redux-observable.
0
Покажите, где в подходе автора стало лучше, чем в redux и более MVC? Я вижу знакомую картину, вид в профиль.
+1
Как на меня, то, что делает автор в топике — полумера. Потому не вижу смысла искать плюсы или минусы в ней. Ну как личный опыт она, конечно, интересна)

Меня лишь крайне огорчают заявления, что редакс — стандарт, на котором стоит останавливаться и не искать далее.
+1
Как вы предлагаете работать с состоянием в react? Выбор то не большой: redux и mobx.
Only those users with full accounts are able to leave comments. , please.