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

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

Под катом — наш перевод статьи*, в которой Патрик сосредоточился на отрицательных примерах: некоторых из худших React-практик, которые он когда-либо видел (а возможно даже использовал сам). Материал призван помочь вам избежать подобных ошибок в будущем.

*Обращаем ваше внимание, что позиция автора может не всегда совпадать с мнением МойОфис.


Ошибка 1. Вы используете prop-drilling

То есть практику передачи одного свойства на несколько уровней от родительского компонента к дочернему.

В идеале, при передаче свойства от родительского компонента к дочернему вы должны избегать проброса более чем на 2 слоя вглубь. (Родительский компонент -> дочерний компонент (слой 1) -> дочерний компонент (слой 2)). В реальности вы можете использовать столько слоев, сколько захотите, но имейте в виду, что prop-drilling обычно вызывает проблемы: во-первых, с производительностью, во-вторых — для последующей поддержки и расширения кодовых баз React.

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

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

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

Ошибка 2. Вы импортируете больше чем нужно

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

// Плохо
import _ from 'lodash'
// Уже лучше
import _get from 'lodash/get'

// Вам это точно нужно?
import _map from 'lodash/map'
// Почему бы не использовать стандартный метод `.map` у массивов?

Чтобы взаимодействие пользователя с приложением происходило успешно, ваша первая отрисовка контента должна быть быстрой (в идеале от 0 до 1,8 секунды), поэтому сократите размер поставляемого в браузер кода, чтобы облегчить ваш бандл. Большинство современных сборщиков приложений, таких как Parcel и Webpack, эффективно минимизируют и сжимают код, но, по-хорошему, вам стоит действительно знать, какой именно код отправляется вашему клиенту, а не просто полагаться на сборщики приложений.

Например, когда был создан lodash, он предоставлял множество функций, отсутствующих в JavaScript. Сегодня JavaScript претерпел серьезные обновления, и lodash лишился своих преимуществ. Почитайте это, чтобы понять, требуется ли применять lodash в вашей ситуации. Разумеется, это зависит в том числе от того, какую версию JavaScript будут использовать ваши пользователи, но в большинстве приложений React установлен Babel или какой-то другой транспилятор, и он позаботится об этом за вас… к тому же в наши дни все поддерживают только Хром… так ведь?

Ошибка 3. Вы не отделяете бизнес-логику от логики компонентов

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

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

Ошибка 4. Вы дублируете работу при каждом рендере

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

import React, { useMemo } from 'react'

const MyMemoExample = ({ items, filter }) => {
  const filteredItems = useMemo(() => {
    return items.filter(filter)
  }, [filter, items])
  
  return (
    { filteredItems.map((item) => <p>{item}</p>)}
  )
} 

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

Ошибка 5. Вы неправильно используете хук useEffect

Хук useEffect, вероятно, является одним из первых хуков, о которых узнает разработчик React. Когда широко использовались классовые компоненты, componentDidMount была обычной функцией жизненного цикла, используемой для назначения слушателей событий; среди хуков ее место занял именно useEffect. Однако неправильное использование хука useEffect приводит к тому, что вы в итоге создаете несколько слушателей событий.

import React, { useEffect } from 'react'


const MyComponent = () => {
  useEffect(() => {
    const buttonClickListener = e => console.log("Clicked ", e)
    document.getElementById("mybutton").addEventListener("click", buttonClickListener)
    return () => {
      document.getElementById("mybutton").removeEventListener("click", buttonClickListener)
    }
  }, [])
  
  return (
    <button id="mybutton">Click Me</button>
  )
}  

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

Ошибка 6. Вы неправильно используете логические операторы

Большинство компонентов имеют несколько логических значений для отображения/не отображения определенных элементов HTML на странице. Это абсолютно нормально. Однако есть несколько разных способов обработки этой логики в компоненте. Чаще всего в компонентах используется оператор '&&'. Это полностью функциональный JavaScript, но он может приводить к некоторым непредвиденным последствиям в вашем пользовательском интерфейсе. Например:

const totalItems = 0

const Component = () => {
    return (
        totalItems && `You have ${totalItems} in your cart` 
    )
}  

Нам требуется, чтобы компонент показывал, сколько товаров у нас в корзине, но в этом сценарии вы в конечном счете увидите на странице просто «0». При использовании синтаксиса условного рендеринга, подобного этому, лучше использовать логическое значение — вместо того, чтобы полагаться на сравнение истинности и ложности JavaScript: последнее хоть и работает, но может вызвать некоторые побочные эффекты в React.


const totalItems = 0

// Вариант 1

const Component2 = () => {
    if (totalItems <= 0) {
        return null
    }
    return (
      <p>
        `You have ${totalItems} in your cart`
      </p>
    )
}

// Вариант 2
const Component3 = () => {
    const hasItems = totalItems > 0
    return (
      <p>
         { hasItems && `You have ${totalItems} in your cart` }
      </p>
    ) 
}  

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

Ошибка 7. Вы повсеместно используете тернарные операторы для условного рендеринга

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

import React, { useEffect } from 'react'


const MyComponent = ({ shouldUseTernary }) => {
  
  return (
    <div>
      This is where a bunch of content would go
      {shouldUseTernary ? (
        <div>
          and this is where optional one content can go 
        </div>
      ): (
        <div>
          or option two who knows
        </div>
      )}
    </div>
  )
}

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

import React from 'react'

function renderingOptions(shouldUseTernary) {

  // позволяет вам даже расширить логику условного рендеринга, если необходимо
  if(shouldUseTernary) {
    return(<div>
           and this is where optional one content can go
    </div>)
  }
  return (
    <div>
    	or option two who knows
    </div>
  )
}

const MyComponent = ({ shouldUseTernary }) => {
  
  return (
    <div>
      This is where a bunch of content would go
    	{renderingOptions(shouldUseTernary)}
    </div>
  )
}

Вариант 1. Используйте функцию для абстрагирования логики рендеринга

import React from 'react'

const FalseTernaryComponent = ({ shouldUseTernary }) => {
  if (shouldUseTernary) {
    return null
  }
  return (
    <div>
      or option two who knows
    </div>
  )
}

const TrueTernaryComponent = ({ shouldUseTernary }) => {
  if (!shouldUseTernary) {
    return null
  }
  return (<div>
    and this is where optional one content can go
  </div>)
}

const MyComponent = ({ shouldUseTernary }) => {
  return (
    <div>
      This is where a bunch of content would go
      <TrueTernaryComponent shouldUseTernary={shouldUseTernary} />
      <FalseTernaryComponent shouldUseTernary={shouldUseTernary} />
    </div>
  )
}

Вариант 2. Пусть компоненты определяют, как они должны отображаться

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

Ошибка 8. Вы не указываете типы свойств или не деструктурируете свойства

По аналогии с большинством вещей в JavaScript, React использует объекты для хранения значений свойств компонентов. То есть внутри объекта может быть передано множество различных значений, которые не будут легко доступны компоненту. Это может затруднить работу над конкретным компонентом.


const BadExample = (props) => {
  return (
    <div>
      {props.title}
      <p>{props.content}</p>
    </div>
  )
}

const BetterExample = (props) => {
  const { title, content } = props
  return (
    <div>
      {title}
      <p>{content}</p>
    </div>
  )
}

const BestExample = ({ title, content }) => (
    <div>
      {title}
      <p>{content}</p>
    </div>
)

BestExample.propTypes = {
  title: PropTypes.string.isRequired,
  content: PropTypes.string.isRequired,
}

Есть разные способы решения такой проблемы. В среде JavaScript использование приведенного выше BestExample дает два преимущества. Прежде всего, вы будете знать, где определен ваш компонент и какие свойства доступны для компонента. Это полезно, так как если вы захотите что-то изменить в будущем, вам не нужно будет долго копаться, чтобы увидеть доступные свойства.

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

Ошибка 9. Вы не используете разделение кода для больших приложений

Большие приложения предполагают большой набор UI-компонентов. Со всеми используемыми пользовательскими компонентами и библиотеками он может быть действительно громоздким. Разделение кода позволяет вам «разбить» ваш бандл на части, которые можно загрузить и запросить позже. То есть, сделать ваш первоначальный бандл меньше, в то время как первая часть кода, необходимая для запуска вашего приложения, будет быстрее доступна для пользователя.

import Loadable from 'react-loadable';
import Loading from './my-loading-component';

const LoadableComponent = Loadable({
  loader: () => import('./my-component'),
  loading: Loading,
});

export default class App extends React.Component {
  render() {
    return <LoadableComponent/>;
  }
}

Код из документации библиотеки react-loadable

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

Итог

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

И в заключение хочу привести известную цитату:

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

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


  1. veocode
    01.09.2022 15:07
    +1

    В первом примере код из 8-го


    1. myoffice_ru
      01.09.2022 15:19
      +7

      Поправили, спасибо!


  1. JustDont
    01.09.2022 19:37

    В кейсе 6 проблема приведенного кода в том, что он НЕ будет распознан как JSX и соответственно не пройдет транспиляцию, превращающую выражение в return в то, что мы на самом деле хотим. А не то, что там в пояснениях написано. Ну и конечно не надо в общих случаях приводить число к boolean, потому что 0 == false.


    1. romanzhivo Автор
      01.09.2022 20:44
      +6

      Подскажите, пожалуйста, почему Вы решили, что код не будет распознан как JSX?

      Проверил, кажется, всё работает корректно https://codepen.io/romanzhivo/pen/eYrOqvv


      1. JustDont
        01.09.2022 21:20
        +4

        Да, пардон, соль все же просто в JS, логические операторы && и || всегда возвращают один из операндов, а не true/false. И в приведенном примере — не тот, который ожидают, и не того типа, на который рассчитывают. Реакт не будет рендерить null, undefined, и false, а всё остальное — как минимум попытается.


  1. Alexandroppolus
    02.09.2022 05:42
    +3

    Довольно распространённый бедпрактис - производное значение в виде useState+useEffect вместо useMemo или просто вычисления. То есть когда какой-то стейт зависит только от некоторых стейтов/пропов/контекстов и может быть синхронно из них посчитан, и при их изменении подправляется через useEffect. Лишний рендер, да и вообще криво.


    1. ivegner
      02.09.2022 17:15
      +1

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

      Но ещё больше бесит, когда useState+useEffect или useMemo используют для вычисления чего-то тривиального, например:

      const isFormValid: boolean = useMemo(
        () => isNameFieldValid && isPasswordFieldValid && isEveryOtherFieldValid,
        [isNameFieldValid, isPasswordFieldValid, isEveryOtherFieldValid]
      );

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

      const isFormValid: boolean = isNameFieldValid && isPasswordFieldValid && isEveryOtherFieldValid;


  1. ivegner
    02.09.2022 17:17

    Не упомянута очень большая ошибка при использовании React, которую совершает автор оригинальной статьи: не использует TypeScript!


  1. avakyansamson
    03.09.2022 11:09

    Заблуждение, что props вызывают rerender в react. Об этом непосредственно в документации сказано.


    1. romanzhivo Автор
      03.09.2022 14:54

      Поясните, пожалуйста что вы имеете в виду?


      1. avakyansamson
        03.09.2022 16:20

        Re-render происходит по 4 триггерам:

        • Изменение свойства в context, на который подписаны

        • Обновление state

        • re-render компонента предка

        • Так же на это могут влиять хуки

        Есть кейс, например, когда re-render не будет, хоть prop изменился. Обновление значения в useRef


        1. romanzhivo Автор
          03.09.2022 22:11

          Спасибо, похоже, тут вопрос в терминологии того, что вы имеете в виду под перерисовкой (re-render).

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

          Или такой пример, где в родительском меняется prop и значение в дочернем тоже меняется, однако, опять же, это те же самые DOM-узлы


  1. GooseOb
    03.09.2022 11:09

    Разве слушатели (5 ошибка) не лучше через onClick весить? Мб тут бы fetch и его abort подошёл бы больше


    1. v-trof
      03.09.2022 13:42

      Однозначно лучше просто заюзать onClick в том случае. И даже если б вы вещали что-то на dom (например потому что оборачиваете leaflet или аналогичную библиотеку) больше смысла взять useLayout effect. И использовать ref, а не id.

      Но у автора вообще есть ряд очень странных советов:

      1) деструктурировать прописы и использовать prop types. Деструктуризация — вкусовщина, мы ее делаем но это ни на что не влияет. PropTypes — слабый runtime аналог typescript.

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