7 запахов кода в React [перевод]

React.js

предисловие

Оригинальная ссылка:7 code smells in your React components

Информация об авторе:Anton Gunnarsson

Разрешение на перевод:

Agree

текст

с момента использованияReactПозже я увидел все больше и больше моментов, которые стоит оптимизировать, например:

  • Многоprops
  • propsнесовместимость
  • propsкопировать какstate
  • вернутьJSXФункция
  • stateнесколько состояний
  • useStateизлишний
  • сложныйuseEffect

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

много реквизита

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

Вопрос в том, сколько это "много"? Ответ - это зависит.

Предположим, вы разрабатываете программу, содержащую 20 или болееpropsкомпонент, когда вы хотите добавить еще несколькоpropsЧтобы улучшить другие функции, в настоящее время есть два момента, на которые следует указать, следует ли разделять компоненты:

Компонент делает больше, чем одну вещь?

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

Например, компонент существуетpropsнесовместимостьиливернутьJSXФункция.

Можно ли синтезировать компонент?

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

Если у вас есть компоненты, в которых несвязанная логика переполнена, пришло время подумать об использовании композиции.

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

<ApplicationForm
  user={userData}
  organization={organizationData}
  categories={categoriesData}
  locations={locationsData}
  onSubmit={handleSubmit}
  onCancel={handleCancel}
  ...
/>

Через этот компонентprops, мы видим, что все они тесно связаны с функциональностью, предоставляемой компонентом.

Компонент выглядит хорошо, но если некоторые изpropsПоделившись им с подкомпонентами, поток данных станет более четким.

<ApplicationForm onSubmit={handleSubmit} onCancel={handleCancel}>
  <ApplicationUserForm user={userData} />
  <ApplicationOrganizationForm organization={organizationData} />
  <ApplicationCategoryForm categories={categoriesData} />
  <ApplicationLocationsForm locations={locationsData} />
</ApplicationForm>

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

Есть ли что рассказать о конфигурацииprops

В некоторых случаях несколько связанных конфигурацийpropsобъединены в одинoptionsэто хорошая практика.

Предположим, у нас есть компонент, который отображает какую-то таблицу:

<Grid
  data={gridData}
  pagination={false}
  autoSize={true}
  enableSort={true}
  sortOrder="desc"
  disableSelection={true}
  infiniteScroll={true}
  ...
/>

Хорошо видно, что эта компонента в дополнение кdataостальныеpropsВсе дело в конфигурации.

Если вы настраиваете несколькоpropsсинтезируется вoptions, вы можете лучше контролировать параметры компонента, а также улучшена спецификация.

const options = {
  pagination: false,
  autoSize: true,
  enableSort: true,
  sortOrder: 'desc',
  disableSelection: true,
  infiniteScroll: true,
  ...
}

<Grid
  data={gridData}
  options={options}
/>

Несовместимость реквизита

Избегайте передачи несовместимых компонентов между компонентамиprops.

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

Ваша реализация может выглядеть так:

function Input({ value, isPhoneNumberInput, autoCapitalize }) {
  if (autoCapitalize) capitalize(value)

  return <input value={value} type={isPhoneNumberInput ? 'tel' : 'text'} />
}

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

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

function TextInput({ value, autoCapitalize }) {
  if (autoCapitalize) capitalize(value)
  useSharedInputLogic()

  return <input value={value} type="text" />
}

function PhoneNumberInput({ value }) {
  useSharedInputLogic()

  return <input value={value} type="tel" />
}

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

реквизит копируется как состояние

как лучшеpropsтак какstateначальное значение .

Имеются следующие компоненты:

function Button({ text }) {
  const [buttonText] = useState(text)

  return <button>{buttonText}</button>
}

Этот компонент будетtextтак какuseStateНачальное значение , может привести к неожиданному поведению.

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

Более практичный сценарий заключается в том, что мы хотимpropsполучить новыйstate.

В приведенном ниже примереslowlyFormatTextфункция для форматированияtext, обратите внимание, что это занимает много времени.

function Button({ text }) {
  const [formattedText] = useState(() => slowlyFormatText(text))

  return <button>{formattedText}</button>
}

Лучшим решением этой проблемы является использованиеuseMemoзаменятьuseState.

function Button({ text }) {
  const formattedText = useMemo(() => slowlyFormatText(text), [text])

  return <button>{formattedText}</button>
}

СейчасslowFormatFormatтолько приtextЗапускается при внесении изменений и не блокирует обновления компонентов верхнего уровня.

Дальнейшее чтение:Writing resilient components by Dan Abramov.

Функции, возвращающие JSX

Не возвращайтесь из функции внутри компонентаJSX.

Этот паттерн встречается редко, но я сталкиваюсь с ним время от времени.

Всего один пример для иллюстрации:

function Component() {
  const topSection = () => {
    return (
      <header>
        <h1>Component header</h1>
      </header>
    )
  }

  const middleSection = () => {
    return (
      <main>
        <p>Some text</p>
      </main>
    )
  }

  const bottomSection = () => {
    return (
      <footer>
        <p>Some footer text</p>
      </footer>
    )
  }

  return (
    <div>
      {topSection()}
      {middleSection()}
      {bottomSection()}
    </div>
  )
}

Хотя этот пример выглядит нормально, на самом деле он нарушает целостность кода и затрудняет обслуживание.

или вернуть функциюJSXВстроить непосредственно в компонент или разделить его на компонент.

Следует отметить одну вещь: если вы создаете новый компонент, вам не нужно перемещать его в новый файл.

Если несколько компонентов тесно связаны, имеет смысл хранить их в одном файле.

несколько состояний состояния

Избегайте использования нескольких логических значений для представления состояния компонента.

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

Например следующий пример:

function Component() {
  const [isLoading, setIsLoading] = useState(false)
  const [isFinished, setIsFinished] = useState(false)
  const [hasError, setHasError] = useState(false)

  const fetchSomething = () => {
    setIsLoading(true)

    fetch(url)
      .then(() => {
        setIsLoading(false)
        setIsFinished(true)
      })
      .catch(() => {
        setHasError(true)
      })
  }

  if (isLoading) return <Loader />
  if (hasError) return <Error />
  if (isFinished) return <Success />

  return <button onClick={fetchSomething} />
}

При нажатии на кнопку мыisLoadingУстановить какtrue, и пройтиfetchВыполнять сетевые запросы.

Если запрос будет успешным, мыisLoadingУстановить какfalse,isFinishedУстановить какtrue, если есть ошибка, тоhasErrorУстановить какtrue.

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

И можно оказаться в «невозможных состояниях», например, когда мы случайно поместилиisLoadingа такжеisFinishedУстановить какtrue.

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

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

function Component() {
  const [state, setState] = useState('idle')

  const fetchSomething = () => {
    setState('loading')

    fetch(url)
      .then(() => {
        setState('finished')
      })
      .catch(() => {
        setState('error')
      })
  }

  if (state === 'loading') return <Loader />
  if (state === 'error') return <Error />
  if (state === 'finished') return <Success />

  return <button onClick={fetchSomething} />
}

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

если вы используетеTypeScriptДля разработки перечисление может быть реализовано с момента определения:

const [state, setState] = useState<'idle' | 'loading' | 'error' | 'finished'>('idle')

слишком много useState

Избегайте использования слишком большого количества компонентов в одном компоненте.useState.

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

Конечно, есть и сложные сценарии, и нам нужно управлять некоторыми сложными состояниями в компоненте.

Ниже приведен пример компонента автоматического ввода:

function AutocompleteInput() {
  const [isOpen, setIsOpen] = useState(false)
  const [inputValue, setInputValue] = useState('')
  const [items, setItems] = useState([])
  const [selectedItem, setSelectedItem] = useState(null)
  const [activeIndex, setActiveIndex] = useState(-1)

  const reset = () => {
    setIsOpen(false)
    setInputValue('')
    setItems([])
    setSelectedItem(null)
    setActiveIndex(-1)
  }

  const selectItem = (item) => {
    setIsOpen(false)
    setInputValue(item.name)
    setSelectedItem(item)
  }

  ...
}

у нас естьresetфункция, которая сбрасывает все состояния, иselectItemфункция для обновления некоторого состояния.

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

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

const initialState = {
  isOpen: false,
  inputValue: "",
  items: [],
  selectedItem: null,
  activeIndex: -1
}
function reducer(state, action) {
  switch (action.type) {
    case "reset":
      return {
        ...initialState
      }
    case "selectItem":
      return {
        ...state,
        isOpen: false,
        inputValue: action.payload.name,
        selectedItem: action.payload
      }
    default:
      throw Error()
  }
}

function AutocompleteInput() {
  const [state, dispatch] = useReducer(reducer, initialState)

  const reset = () => {
    dispatch({ type: 'reset' })
  }

  const selectItem = (item) => {
    dispatch({ type: 'selectItem', payload: item })
  }

  ...
}

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

Дальнейшее чтение:state reducer pattern by Kent C. Dodds.

Комплексное использованиеЭффект

избегать вuseEffectОни делают код подверженным ошибкам и трудным для понимания.

Следующий пример делает большую ошибку:

function Post({ id, unlisted }) {
  ...

  useEffect(() => {
    fetch(`/posts/${id}`).then(/* do something */)

    setVisibility(unlisted)
  }, [id, unlisted])

  ...
}

когдаunlistedменяться, даже еслиidБез изменений, тоже позвонюfetch.

Правильный способ написать это должен состоять в том, чтобы разделить несколько зависимостей:

function Post({ id, unlisted }) {
  ...

  useEffect(() => { // when id changes fetch the post
    fetch(`/posts/${id}`).then(/* ... */)
  }, [id])

  useEffect(() => { // when unlisted changes update visibility
    setVisibility(unlisted)
  }, [unlisted])

  ...
}

заключительные замечания

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

Если вы обнаружите другие проблемные шаблоны, не стесняйтесь комментировать или наTwitterСвяжитесь со мной по.