Когда речь идёт о разработке React-приложений, то, в плане архитектуры кода, маленькие проекты часто бывают более гибкими, чем большие. Нет ничего плохого в том, чтобы создавать такие проекты с использованием практических рекомендаций, нацеленных на более крупные приложения. Но всё это, в случае с маленькими проектами, может оказаться попросту ненужным. Чем меньше приложение — тем «снисходительнее» оно относится к использованию в нём простых решений, возможно — неоптимальных, но не требующих больших затрат времени на их реализацию.



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

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

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

Но тут подкралось будущее.

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

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

Библиотека Redux, если правильно использовать её в больших проектах, помогает, по мере роста таких проектов, сохранять их код в поддерживаемом состоянии.

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

1. Не размещайте код действий и констант в одном месте


Вы могли столкнуться с некоторыми руководствами по Redux, в которых константы и все действия размещают в одном и том же месте. Однако подобный подход, по мере роста приложения, быстро может привести к появлению проблем. Константы нужно хранить отдельно, например, в ./src/constants. В результате для поиска констант придётся заглядывать лишь в одну папку, а не в несколько.

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

Предположим, вы разрабатываете аркадную или ролевую игру и создаёте классы warrior (воин), sorceress (волшебница) и archer (лучник). В такой ситуации добиться высокого уровня поддерживаемости кода можно, организовав действия следующим образом:

src/actions/warrior.js
src/actions/sorceress.js
src/actions/archer.js

Гораздо хуже будет, если всё попадёт в один файл:

src/actions/classes.js

Если приложение становится очень большим, то, возможно, ещё лучше будет воспользоваться примерно такой структурой разбиения кода по файлам:

src/actions/warrior/skills.js
src/actions/sorceress/skills.js
src/actions/archer/skills.js

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

src/actions/warrior/skills.js
src/actions/warrior/quests.js
src/actions/warrior/equipping.js
src/actions/sorceress/skills.js
src/actions/sorceress/quests.js
src/actions/sorceress/equipping.js
src/actions/archer/skills.js
src/actions/archer/quests.js
src/actions/archer/equipping.js

Вот как может выглядеть действие из файла src/actions/sorceress/skills для объекта sorceress:

import { CAST_FIRE_TORNADO, CAST_LIGHTNING_BOLT } from '../constants/sorceress'

export const castFireTornado = (target) => ({
  type: CAST_FIRE_TORNADO,
  target,
})

export const castLightningBolt = (target) => ({
  type: CAST_LIGHTNING_BOLT,
  target,
})

Вот содержимое файла src/actions/sorceress/equipping:

import * as consts from '../constants/sorceress'

export const equipStaff = (staff, enhancements) => {...}

export const removeStaff = (staff) => {...}

export const upgradeStaff = (slot, enhancements) => {
  return (dispatch, getState, { api }) => {
    // Обратиться к слоту на экране обмундирования для того чтобы получить ссылку на посох волшебницы
    const state = getState()
    const currentEquipment = state.classes.sorceress.equipment.current
    const staff = currentEquipment[slot]
    const isMax = staff.level >= 9
    if (isMax) {
      return
    }
    dispatch({ type: consts.UPGRADING_STAFF, slot })

    api.upgradeEquipment({
      type: 'staff',
      id: currentEquipment.id,
      enhancements,
    })
    .then((newStaff) => {
      dispatch({ type: consts.UPGRADED_STAFF, slot, staff: newStaff })
    })
    .catch((error) => {
      dispatch({ type: consts.UPGRADE_STAFF_FAILED, error })
    })
  }
}

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

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

2. Не размещайте код редьюсеров в одном месте


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

const equipmentReducers = (state, action) => {
  switch (action.type) {
    case consts.UPGRADING_STAFF:
      return {
        ...state,
        classes: {
          ...state.classes,
          sorceress: {
            ...state.classes.sorceress,
            equipment: {
              ...state.classes.sorceress.equipment,
              isUpgrading: action.slot,
            },
          },
        },
      }
    case consts.UPGRADED_STAFF:
      return {
        ...state,
        classes: {
          ...state.classes,
          sorceress: {
            ...state.classes.sorceress,
            equipment: {
              ...state.classes.sorceress.equipment,
              isUpgrading: null,
              current: {
                ...state.classes.sorceress.equipment.current,
                [action.slot]: action.staff,
              },
            },
          },
        },
      }
    case consts.UPGRADE_STAFF_FAILED:
      return {
        ...state,
        classes: {
          ...state.classes,
          sorceress: {
            ...state.classes.sorceress,
            equipment: {
              ...state.classes.sorceress.equipment,
              isUpgrading: null,
            },
          },
        },
      }
    default:
      return state
  }
}

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

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

3. Используйте информативные имена переменных


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

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

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

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

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

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

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

Я поискал в коде по словам info, dataToSend, dataObject, и по другим, которые, в моём представлении, связаны с данными, получаемыми с сервера. Через 5-10 минут мне удалось найти код, ответственный за работу с нужными мне данными. Объект, в котором они оказывались, был назван paymentObject. В моём представлении объект, имеющий отношение к платежам, может содержать нечто вроде CVV-кода, номера кредитной карты, почтового индекса плательщика, и другие подобные сведения. В обнаруженном мной объекте было 11 свойств. Лишь три из них имели отношение к платежам: метод оплаты, идентификатор платёжного профиля и список кодов купонов.

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

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

import React from 'react'

class App extends React.Component {
  state = { data: null }

  // Кого уведомляем-то?
  notify = () => {
    if (this.props.user.loaded) {
      if (this.props.user.profileIsReady) {
        toast.alert(
          'You are not approved. Please come back in 15 minutes or you will be deleted.',
          {
            position: 'bottom-right',
            timeout: 15000,
          },
        )
      }
    }
  }

  render() {
    return this.props.render({
      ...this.state,
      notify: this.notify,
    })
  }
}

export default App

4. Не изменяйте структуры данных или типы в уже настроенных потоках данных приложений


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

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

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

5. Используйте сниппеты


Раньше я был фанатом редактора Atom, но перешёл на VS Code из-за того, что этот редактор, в сравнении с Atom, оказался невероятно быстрым. И он, при его скорости, поддерживает огромное количество самых разных возможностей.

Если вы тоже пользуетесь VS Code — рекомендую установить расширение Project Snippets. Это расширение позволяет программисту создавать собственные сниппеты для каждого рабочего пространства, используемого в некоем проекте. Это расширение работает так же, как и встроенный в VS Code механизм Use Snippets. Разница заключается в том, что при работе с Project Snippets в проекте создают папку .vscode/snippets/. Выглядит это так, как показано на следующем рисунке.


Содержимое папки .vscode/snippets/

6. Создавайте модульные, сквозные и интеграционные тесты


По мере роста размеров приложения программисту становится всё страшнее редактировать код, который не покрыт тестами. Например, может случиться так, что некто отредактировал код, хранящийся в src/x/y/z/, и решил отправить его в продакшн. Если при этом внесённые изменения влияют на те части проекта, о которых программист не подумал, всё может закончиться ошибкой, с которой столкнётся реальный пользователь. Если в проекте имеются тесты, программист узнает об ошибке задолго до того, как код попадёт в продакшн.

7. Проводите мозговые штурмы


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

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

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

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

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

8. Создавайте макеты приложений


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

Moqups — это одно из средств для создания макетов приложений, о котором мне часто приходится слышать. Это — быстрый инструмент, созданный средствами HTML5 и JavaScript и не предъявляющий особых требований к системе.

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

9. Планируйте поток данных в приложениях


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

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

10. Используйте функции доступа к данным


По мере роста размеров приложения растёт и количество его компонентов. А когда растёт количество компонентов, то же самое происходит и с частотой использования селекторов (react-redux ^v7.1) или mapStateToProps. Предположим, вы обнаруживаете, что ваши компоненты или хуки часто обращаются к фрагментам состояния в различных частях приложения с использованием конструкции наподобие useSelector((state) => state.app.user.profile.demographics.languages.main). Если так — это значит, что вам нужно подумать о создании функций доступа к данным. Файлы с такими функциями стоит хранить в общедоступном месте из которого их могут импортировать компоненты и хуки. Подобные функции могут быть фильтрами, парсерами, или любыми другими функциями для трансформации данных

Вот несколько примеров.

Например, в src/accessors может присутствовать такой код:

export const getMainLanguages = (state) =>
  state.app.user.profile.demographics.languages.main

Вот версия с использованием connect, которая может быть расположена по пути src/components/ViewUserLanguages:

import React from 'react'
import { connect } from 'react-redux'
import { getMainLanguages } from '../accessors'

const ViewUserLanguages = ({ mainLanguages }) => (
  <div>
    <h1>Good Morning.</h1>
    <small>Here are your main languages:</small>
    <hr />
    {mainLanguages.map((lang) => (
      <div>{lang}</div>
    ))}
  </div>
)

export default connect((state) => ({
  mainLanguages: getMainLanguages(state),
}))(ViewUserLanguages)

Вот версия, в которой применяется useSelector, находящаяся по адресу src/components/ViewUserLanguages:

import React from 'react'
import { useSelector } from 'react-redux'
import { getMainLanguages } from '../accessors'

const ViewUserLanguages = ({ mainLanguages }) => {
  const mainLanguages = useSelector(getMainLanguages)

  return (
    <div>
      <h1>Good Morning.</h1>
      <small>Here are your main languages:</small>
      <hr />
      {mainLanguages.map((lang) => (
        <div>{lang}</div>
      ))}
    </div>
  )
}

export default ViewUserLanguages

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

11. Управляйте потоком данных в свойствах с помощью деструктурирования и синтаксиса spread


Каковы преимущества использования конструкции props.something перед конструкцией something?

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

const Display = (props) => <div>{props.something}</div>

Вот — то же самое, но уже с использованием деструктурирования:

const Display = ({ something }) => <div>{something}</div>

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

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

const Display = ({ something = 'apple' }) => <div>{something}</div>

Возможно, раньше вы видели что-то подобное следующему примеру:

const Display = (props) => (
  <Agenda {...props}>
    {' '}
    // перенаправление других свойств компоненту Agenda
    <h2><font color="#3AC1EF">Today is {props.date}</font></h2>
    <hr />
    <div>
      <h3><font color="#3AC1EF">?Here your list of todos:</font></h3>
      {props.children}
    </div>
  </Agenda>
)

Подобные конструкции непросто читать, но это — не единственная их проблема. Так, здесь имеется ошибка. Если приложение выводит и дочерние компоненты, то props.children отображается на экране дважды. Если работа над проектом ведётся в команде и члены команды недостаточно внимательны, вероятность возникновения подобных ошибок довольно-таки высока.

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

const Display = ({ children, date, ...props }) => (
  <Agenda {...props}>
    {' '}
    // перенаправление других свойств компоненту Agenda
    <h2><font color="#3AC1EF">Today is {date}</font></h2>
    <hr />
    <div>
      <h3><font color="#3AC1EF">?Here your list of todos:</font></h3>
      {children}
    </div>
  </Agenda>
)

Итоги


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

Уважаемые читатели! Какие советы вы добавили бы к тем, что приведены в этой статье?



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


  1. Ungla
    19.06.2019 17:30

    1 — как-то очевидно
    2 — Лучше бы звучало как «держите state максимально плоским»
    Начиная с 3 и до конца, советы, прямо скажем, не про Redux.


    1. in19farkt
      19.06.2019 19:20

      ага, 4 это про рефакторинг javascript кода в принципе. Используйте в своих проектах TypeScript и таких проблем никогда не будет))


  1. xhumanoid
    19.06.2019 19:35
    +2

    обман, везде обман…

    сказали что 12 советов, но перечислили 11, так как 5й совет пропустили

    troll mode activated: интересно, а если у вас на год взять сервер, то вместо обещанных 12 месяцев тоже получишь 11?


    1. ru_vds Автор
      19.06.2019 23:17
      +1

      Спасибо за комментарий. Ошибка с 12 советами идет из оригинала, там также пропущен 5 пункт. Статью и заголовок поправили.

      marketing mode activated: Что касается оплаты сервера за год, то вы точно получите 12 месяцев, а в июне еще и скидку в 30%, плюс подарки от Ричарда Levelord'a.


      1. superyarik
        20.06.2019 12:39
        +1

        в итогах осталось 12


  1. faiwer
    19.06.2019 23:12

    Я спустя 3-4 года возни с redux пришёл к таким выводам:


    • Action types не только не нужно хранить отдельно от action creators. Их вообще не нужно делать. Для чего они могут пригодится? Для того чтобы иметь в рамках одного типа несколько фабрик action-ов. На моей практике такой случай не наступил ни разу. Могу смело сказать, что в 99% это просто бойлерплейт. Мусор. Мусору нечего делать в кодовой базе. Хотите глоссарий экшнов — автоматизируйте это. Как связывать reducer-ы с action-ами? Очень просто импортировать actionCreator-ы там где у вас reducer-ы и цепляться к ранее установленным (автоматически) actionCreator.type. Эти не сильно простые шаги уменьшат боль при написании redux приложений раза в 2.


    • Держите в простых случаев reducer-ы там же где и actionCreator-ы. А сложных случаев избегайте (дробите). Может показаться странным, но это радикально уменьшает количество бойлерплейта и упрощает поддержку кода. При этом различные слои не путаются, просто лежат ближе. Недостатков пока не выявил. А вот разного рода селекторы бывает полезным вынести отдельно, т.к. их иногда может 3\4 от кода набраться и они не привязаны ни к каким экшн-тайпам.


    • Используйте proxy в reducer-ах. Это невероятным образом улучшает как кодовую базу, так и экономит ваше время. Мутабельный код с преимуществами иммутабельного. Лучшее что я видел для redux. Реализация довольно простая, да и есть готовые решения. После внедрения бизнес-логика перестаёт выглядеть как тихий ужас.


    • Забудте про switch-case. Ден привёл в качестве примера и понеслась. Преимуществ у них похоже никаких, но все их пишут и пишут. Куда удобнее держать привязку action-type -> handler(state, action, rootState)


    • 1 action = 1 handler. Не прогонять каждый action через все reducer-ы. Принудительно привязывать обработку задачи к одному handler-у. Делает код куда более предсказуемым. Это одна из важнейших вещей в redux-е. Как это обеспечить? Ну например держать путь к reducer-у в action.type. Хотя тут много вариаций.


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


    • Используйте нормализацию. Ну тут много про это писали. Это архиважно


    • Используйте memo, useAutoCallback, useAutoMemo, PureComponent и прочие примитивы с shallowComparison. Это азбука производительности вашего приложения. Очень помогут weakMemoize-ы разного рода. Селекторы, линзы и прочее. Обычно в серьёзном приложении таких утилит\помощников скапливается много.



    1. faiwer
      19.06.2019 23:16

      Забыл добавить ещё 1 важный пункт. У меня всегда большая часть actionCreator-ов сводилась к:


      • получить список параметров
      • упаковать их в POJO дав правильные имена
      • приправить полем type

      Это можно хорошо оптимизировать. Убрав метод вообще, сделав фабрику фабрик (creator of action creator). Можно даже валидацию добавить (актуально для не TS | Flow).


      Уменьшает бойлерплейт значительно. Большинство таких actionCreator-ов легко помещаются в 1 строку. Учитывая отсутствие actionType-ов из других файлов, отдельных к ним импортов, и лишних экспортов… Можно приблизиться по уровню комфорта к норма...решениям на основе observable.


    1. Ungla
      21.06.2019 12:52

      А можно пример «Забудте про switch-case» и «1 action = 1 handler»? Буду очень признателен.


      1. faiwer
        21.06.2019 16:23
        -1

        Забудте про switch-case

        Смотрите. Что нам даёт конструкция switch-case? Разветвление. В случае reducer-ов разветвление по action.type. Чем хороша switch-case? В ней есть waterfall (когда 2 case имеют один кодовый блок). Чем плоха? Не самая удачная конструкция. Область видимости и вообще слишком разбухшая структура совсем не связанных между собой блоков кода. Чем плоха в случае reducer-ов? Тем что у нас итак reducer (если без proxy) из-за иммутабельности выглядят ужасно (сплошные портянки ..., какие-нибудь immutable callback-и, самописные методы и пр.), а тут ещё и вся лапша в одном месте на много-много строк. Зачастую на сотни строк. При этом мы не используем waterfall и по факту каждый case занимается своим и только своим случаем.


        Что можно сделать взамен? Каждый case вынести в отдельный метод. Например:


        switch(action.type) {
          case ACTION_1.type: return handler1(state, action);
          case ACTION_2.type: return handler2(state, action);
          default: return state;
        }
        
        const handler1 = (state, action) => { /* some logic */ };

        Можно так:


        const map = {
          [ACTION_1]: (state, action) => { /* logic */ },
          [ACTION_2]: importedHandler2,
          [ACTION_3]: (state, action) => {
            /* logic A */
            state = map[ACTION_2](state, action); // logic B
            /* logib C */
          }
        };
        
        export default (state, action) => { // reducer
          if (action.type in map) 
            return map[action.type](state, action);
          return state;
        }

        Можно и лучше. Бойлерплейт вынести в обёртки. Особенно если хочется подключить proxy.


        1 action = 1 handler

        Типовой каноничный redux-проект прогоняет ваш action через все ваши reducer-ы и все case-ы в switch-ах. Какие в этом преимущества? Оно одно: можно за-dispatch-ить 1 action, а изменить store в нескольких местах. Хорошо это или нет? Я считаю это зло, т.к. делает работу со store куда менее предсказуемой и прямолинейной. Особенно если изменение куска стора не гарантировано (скажем проверка по какому-нибудь флагу).


        Как можно сделать иначе? Куча способов. Опишу самый простой (использую его везде сам). В action.type держать не просто уникальную строку, а строку с путём. PATH1/PATH2/PATH3/ACTION. И вместо combineReducer использовать сопоставление с сегментом этого пути. Тогда мы приходим к схеме 1 action = 1 блок кода который его обрабатывает. А reducer-ы куда в большей степени декларативны.


        Какие недостатки у этого подхода?


        1. нельзя изменять стор сразу в 2 местах. Как решать? 2 action-а. На мой взгляд такой путь куда лучше подходит идее redux-а. Явное лучше неявного. Причём лучше вызывать 1 meta-actionCreator, который сделает dispatch нескольких. По правде говоря я с этим ограничением ещё не столкнулся. Мне кажется это хороший признак правильно построенной архитектуры.
        2. action-ы в большей степени привязаны к структуре store-а и reducer-ам. Это противоречит идеологии redux, когда action-ы не должны ничего знать о store и его структуре. Они ведь только намерения. Но тут я могу легко заметить, что это идеологическая картинка у нас и без того рушится в любом приложении, т.к. народ чуть ли не всю бизнес логику наворачивает в этих самых actionCreator-ах и вообще активно использует там getState. Да и actionTypes кладут рядом с reducer-ами (даже если в отдельный файл). Т.е. это яркий пример того, что на бумажке идея хороша, а проверку практикой не выдерживает. Т.е. то что идеологически обособленно почти гарантированно у всех оказывается связанным. Так что я тут просто предлагаю не притворяться, что эти 2 слоя с друг другом не связаны.

        Вообще когда перестаёшь "притворяться" в redux радикально падает количество бойлерплейта. Вот первый же совет в этой статье идеологический. Если сделать в точности наоборот (а лучше вообще избавиться от action-type-ов) то в реальных проектах улучшается всё: разработка, поддержка, рефакторинг.


        Я вначале старался слепо следовать идеологии redux во всех пунктах и постепенно наступил на все возможные грабли. Много переосмыслил, взвесил и построил по-другому. На данный момент ни о чём не жалею, обновлённая схема оказывается куда удобнее в разработке. Суть однонаправленного подхода остаётся прежней. Количество бойлерплейта падает в 3-10 раз (зависит от степени упоротости).


        1. Ungla
          21.06.2019 17:36

          Спасибо больше за развёрнутый ответ.
          Я оказывается использовал такое, с таким хэлпером:

          export default function createReducer(initialState, handlers) {
              return (state = initialState, action) => {
                  if (handlers.hasOwnProperty(action.type)) {
                      return handlers[action.type](state, action)
                  } else {
                      return state
                  }
              }
          }


          «Поменяйте сигнатуру каждого редьюсера с (state, action) на (state, action, rootState)» вот это ещё заинтриговало, не могу представить как туда ещё и стор весь передать.

          Хотел ещё добавить про Action types, их удобно держать отдельно, если используется redux-saga


          1. faiwer
            21.06.2019 19:27
            -1

            «Поменяйте сигнатуру каждого редьюсера с (state, action) на (state, action, rootState)» вот это ещё заинтриговало, не могу представить как туда ещё и стор весь передать

            На самом верху вам передаётся rootStore как первый аргумент. Если проконтролировать всё древо reducer-ов то вы можете не терять его. Можно скажем написать кастомный combineReducers с 3 параметрами (я так делал поначалу).


            Касательно саг — не работал с ними, для меня это другой мир. Но кажется саги нужны там, где в более простых случаях пишут async-actionCreator-ы. А это по сути совсем другая сущность (во vuex её отделили даже). И там наверняка вы можете прибегнуть к тем же actionCreator.type (или .toString) без проблем.


  1. Finesse
    20.06.2019 08:38

    Не изменяйте структуры данных или типы в уже настроенных потоках данных приложений

    [...]

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

    Redux in a nutshell (редакс в двух словах). Эту проблему помогает решать TypeScript, но даже с ним рефакторинг модуля требует большой внимательности.


  1. 1c80
    23.06.2019 17:40

    А mobx разве не лучше использовать?