Задний план

Фундаментальная концепция React заключается в том, что вы всегда должны использовать компоненты. Это означает, что приложение всегда должно быть разбито на множество более мелких компонентов, а не на один гигантский компонент. Это поможет улучшить ремонтопригодность и масштабируемость в долгосрочной перспективе по сравнению с размещением всего вашего кода в одном файле в краткосрочной перспективе. Несмотря на то, что как разбить ваше приложение на компоненты, несложно, у меня возник вопрос, когда следует разбивать на компоненты. Это произошло в моем проекте Group Randomizer, где я выгрузил большинство своих функций и состояний в файл App.js.

Воссоздание проблемы

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

//App.js
function App() {
  hook for stateOne
  hook for stateTwo
  hook for stateThree
  hook for stateFour
  hook for stateFive
  function alpha (stateOne) 
  function bravo (stateOne, stateTwo)
  function charlie (stateThree)
  function delta (stateFour)
  function echo (stateThree, stateFive)
return (
    <div>
      renders something based on the output of function alpha
      renders something based on the output of function bravo
      renders something based on the output of function charlie
      renders something based on the output of function delta
      renders something based on the output of function echo
    </div>
  )
}

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

Всегда быть компонентным

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

Псевдокод будет выглядеть следующим образом.

//App.js
import ComponentOne from './ComponentOne'
function App() {
  hook for stateTwo
  hook for stateThree
  hook for stateFour
  hook for stateFive
  function bravo (stateOne, stateTwo)
  function charlie (stateThree)
  function delta (stateFour)
  function echo (stateThree, stateFive)
return (
    <div>
      <ComponentOne />
      renders something based on the output of function bravo
      renders something based on the output of function charlie
      renders something based on the output of function delta
      renders something based on the output of function echo
    </div>
  )
}
//ComponentOne.js
export default function ComponentOne() {
  hook for stateOne
  function alpha (stateOne)
  return (
    <div>
      renders something based on the output of function alpha
    </div>
  )
}

По сути, я переместил хук для stateOne и function alpha из App.js в ComponentOne.js, потому что они напрямую затрагивали первый компонент. Пока это выглядит не так уж и плохо. Переходим ко второму компоненту.

//ComponentTwo.js
export default function ComponentTwo() {
  hook for stateOne
  hook for stateTwo
  function bravo (stateOne, stateTwo)
  return (
    <div>
      renders something based on the output of function bravo
    </div>
  )
}

Что ж, мы уже столкнулись с первой загвоздкой. Хук stateOne уже определен в ComponentOne.js. Репликация одного и того же хука в другом файле нарушит принцип единой ответственности и может привести к множеству побочных эффектов/ошибок в будущем.

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

//App.js
import ComponentOne from './ComponentOne'
function App() {
  hook for stateOne
  hook for stateThree
  hook for stateFour
  hook for stateFive
  function charlie (stateThree)
  function delta (stateFour)
  function echo (stateThree, stateFive)
  function updateStateOneCallback()
  return (
    <div>
      <ComponentOne stateOne={stateOne} updateStateOneCallback={updateStateOneCallback}/>
      <ComponentTwo stateOne={stateOne} />
      renders something based on the output of function charlie
      renders something based on the output of function delta
      renders something based on the output of function echo
    </div>
  )
}
//ComponentOne.js
export default function ComponentOne(props) {
  function alpha (props.stateOne)
  return (
    <div>
      renders something based on the output of function alpha
      <button onClick={props.updateStateOneCallback}>Click Me</button>
    </div>
  )
}
//ComponentTwo.js
export default function ComponentTwo(props) {
  hook for stateTwo
  function bravo (props.stateOne, stateTwo)
  return (
    <div>
      renders something based on the output of function bravo
    </div>
  )
}

Масштабирование проблемы

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

Кроме того, мы передавали реквизиты и функции обратного вызова между App, ComponentOne и ComponentTwo, где App — родительский компонент ComponentOne и ComponentTwo. Нам пришлось это сделать, потому что stateOne повлияло на ComponentOne и ComponentTwo. Но что, если stateOne или любое другое состояние влияет на вложенные компоненты, как в компоненте, который является праправнуком App? Нам нужно будет передавать реквизиты и функции обратного вызова пропорционально тому, насколько глубоко вложен компонент.

App (where stateOne lives)
|
|- ComponentOne
|       |
|       |- ComponentOneChild
|                     |
|                     |- ComponentOneGrandChild
|                                     |
|                                     |- ComponentOneGreatGrandChild 
|                                          (needs to use stateOne)
|- ComponentTwo                  
|
|- ComponentThree

На приведенной выше диаграмме показано, как может выглядеть вложенный компонент для stateOne .

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

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

Вернуться к исходному выпуску

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

Я считаю, что вы должны начать компонентную обработку, когда:

  1. Введено новое состояние, которое может повлиять на несколько компонентов
  2. Часть кода имеет одну конкретную цель (например, заголовок, панель навигации, графики и т. д.).

Чтобы вернуться к примеру App и подчеркнуть второй момент, часть кода отображает результат function alpha . Поскольку function alpha не влияет на остальную часть кода, имеет логический смысл разделить на компоненты рендеринг результатов function alpha и самой функции, следовательно, создание ComponentOne, и использовать Redux для отслеживания stateOne, потому что stateOne также влияет на function bravo, который находится в ComponentTwo .

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

Ключевые выводы

  1. Компонентизируйте по мере необходимости, как описано в предыдущем разделе.
  2. Каркас приложения перед началом кодирования.

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

3. Используйте Redux или любые инструменты управления состоянием как можно скорее, чтобы помочь вам при компонентизации.

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