React: лучшие практики

Автор оригинала: Alex Kondov
  • Перевод


Разрабатываете на React или просто интересуетесь данной технологией? Тогда добро пожаловать в мой новый проект — Тотальный React.

Введение


Я работаю с React уже 5 лет, однако, когда дело касается структуры приложения или его внешнего вида (дизайна), сложно назвать какие-то универсальные подходы.

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

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

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

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

Компоненты


Функциональные компоненты

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

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

//  Классовые компоненты являются "многословными"
class Counter extends React.Component {
  state = {
    counter: 0,
  }

  constructor(props) {
    super(props)
    this.handleClick = this.handleClick.bind(this)
  }

  handleClick() {
    this.setState({ counter: this.state.counter + 1 })
  }

  render() {
    return (
      <div>
        <p>Значение счетчика: {this.state.counter}</p>
        <button onClick={this.handleClick}>Увеличить</button>
      </div>
    )
  }
}

//  Функциональные компоненты легче читать и поддерживать
function Counter() {
  const [counter, setCounter] = useState(0)

  handleClick = () => setCounter(counter + 1)

  return (
    <div>
      <p>Значение счетчика: {counter}</p>
      <button onClick={handleClick}>Увеличить</button>
    </div>
  )
}

Согласованные (последовательные) компоненты

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

У каждого подхода имеются свои преимущества и недостатки.

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

Названия компонентов

Всегда именуйте компоненты. Это помогает анализировать трассировку стека ошибки при использовании инструментов разработчика React.

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

//  Этого следует избегать
export default () => <form>...</form>

//  Именуйте свои функции
export default function Form() {
  return <form>...</form>
}

Вспомогательные функции

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

Это уменьшает «шум» компонента — в нем остается только самое необходимое.

//  Этого следует избегать
function Component({ date }) {
  function parseDate(rawDate) {
    ...
  }

  return <div>Сегодня {parseDate(date)}</div>
}

//  Размещайте вспомогательные функции перед компонентом
function parseDate(date) {
  ...
}

function Component({ date }) {
  return <div>Сегодня {parseDate(date)}</div>
}

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

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

//  Вспомогательные функции не должны "читать" значения из состояния компонента
export default function Component() {
  const [value, setValue] = useState('')

  function isValid() {
    // ...
  }

  return (
    <>
      <input
        value={value}
        onChange={e => setValue(e.target.value)}
        onBlur={validateInput}
      />
      <button
        onClick={() => {
          if (isValid) {
            // ...
          }
        }}
      >
        Отправить
      </button>
    </>
  )
}

//  Поместите их снаружи и передавайте им только необходимые значения
function isValid(value) {
  // ...
}

export default function Component() {
  const [value, setValue] = useState('')

  return (
    <>
      <input
        value={value}
        onChange={e => setValue(e.target.value)}
        onBlur={validateInput}
      />
      <button
        onClick={() => {
          if (isValid(value)) {
            // ...
          }
        }}
      >
        Отправить
      </button>
    </>
  )
}

Статическая (жесткая) разметка

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

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

//  Статическую разметку сложно поддерживать
function Filters({ onFilterClick }) {
  return (
    <>
      <p>Жанры книг</p>
      <ul>
        <li>
          <div onClick={() => onFilterClick('fiction')}>Научная фантастика</div>
        </li>
        <li>
          <div onClick={() => onFilterClick('classics')}>
            Классика
          </div>
        </li>
        <li>
          <div onClick={() => onFilterClick('fantasy')}>Фэнтези</div>
        </li>
        <li>
          <div onClick={() => onFilterClick('romance')}>Романы</div>
        </li>
      </ul>
    </>
  )
}

//  Используйте циклы и объекты с настройками
const GENRES = [
  {
    identifier: 'fiction',
    name: 'Научная фантастика',
  },
  {
    identifier: 'classics',
    name: 'Классика',
  },
  {
    identifier: 'fantasy',
    name: 'Фэнтези',
  },
  {
    identifier: 'romance',
    name: 'Романы',
  },
]

function Filters({ onFilterClick }) {
  return (
    <>
      <p>Жанры книг</p>
      <ul>
        {GENRES.map(genre => (
          <li>
            <div onClick={() => onFilterClick(genre.identifier)}>
              {genre.name}
            </div>
          </li>
        ))}
      </ul>
    </>
  )
}

Размеры компонентов

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

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

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

Полагайтесь на пропы и коллбеки для взаимодействия и получения данных. Количество строк кода далеко не всегда является объективным критерием его качества. Всегда помните об отзывчивости и абстракции.

Комментарии в JSX

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

function Component(props) {
  return (
    <>
      {/* Если пользователь оформил подписку, мы не будем показывать ему рекламу */}
      {user.subscribed ? null : <SubscriptionPlans />}
    </>
  )
}

Предохранители

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

В функции, запрашивающей данные, у нас может быть любое количество блоков «try/catch». Используйте предохранители не только на верхнем уровне приложения, но оборачивайте им каждый компонент, в котором потенциально может быть выброшено исключение во избежание каскада ошибок.

function Component() {
  return (
    <Layout>
      <ErrorBoundary>
        <CardWidget />
      </ErrorBoundary>

      <ErrorBoundary>
        <FiltersWidget />
      </ErrorBoundary>

      <div>
        <ErrorBoundary>
          <ProductList />
        </ErrorBoundary>
      </div>
    </Layout>
  )
}

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

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

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

//  Не повторяйте "props" в каждом компоненте
function Input(props) {
  return <input value={props.value} onChange={props.onChange} />
}

//  Деструктурируйте и используйте значения в явном виде
function Component({ value, onChange }) {
  const [state, setState] = useState('')

  return <div>...</div>
}

Количество пропов

Ответ на вопрос о количестве пропов является очень субъективным. Количество пропов, передаваемых в компонент, коррелируется с количеством используемых компонентом переменных. Чем больше пропов передается в компонент, тем выше его ответственность (имеется ввиду количество решаемых компонентом задач).

Большое количество пропов может свидетельствовать о том, что компонент делает слишком много.

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

Обратите внимание: чем больше пропов принимает компонент, чем чаще он перерисовывается.

Передача объекта вместо примитивов

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

//  Не передавайте значения по одному
<UserProfile
  bio={user.bio}
  name={user.name}
  email={user.email}
  subscription={user.subscription}
/>

//  Вместо этого, используйте объект
<UserProfile user={user} />

Условный рендеринг

В некоторых случаях использование коротких вычислений (оператора «логическое И» — &&) для условного рендеринга может привести к отображению 0 в UI. Во избежание этого используйте тернарный оператор. Единственным недостатком такого подхода является чуть большее количество кода.

Оператор "&&" уменьшает количество кода, что здорово. Тернарник является более «многословным», зато всегда работает корректно. Кроме того, добавление альтернативного варианта при необходимости становится менее трудоемким.

//  Старайтесь избегать коротких вычислений
function Component() {
  const count = 0

  return <div>{count && <h1>Сообщения: {count}</h1>}</div>
}

//  Вместо этого, используйте тернарный оператор
function Component() {
  const count = 0

  return <div>{count ? <h1>Сообщения: {count}</h1> : null}</div>
}

Вложенные тернарные операторы

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

//  Вложенные тернарники сложно читать
isSubscribed ? (
  <ArticleRecommendations />
) : isRegistered ? (
  <SubscribeCallToAction />
) : (
  <RegisterCallToAction />
)

//  Извлеките их в отдельный компонент
function CallToActionWidget({ subscribed, registered }) {
  if (subscribed) {
    return <ArticleRecommendations />
  }

  if (registered) {
    return <SubscribeCallToAction />
  }

  return <RegisterCallToAction />
}

function Component() {
  return (
    <CallToActionWidget
      subscribed={subscribed}
      registered={registered}
    />
  )
}

Списки

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

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

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

//  Не объединяйте циклы с другой разметкой
function Component({ topic, page, articles, onNextPage }) {
  return (
    <div>
      <h1>{topic}</h1>
      {articles.map(article => (
        <div>
          <h3>{article.title}</h3>
          <p>{article.teaser}</p>
          <img src={article.image} />
        </div>
      ))}
      <div>Вы находитесь на странице {page}</div>
      <button onClick={onNextPage}>Дальше</button>
    </div>
  )
}

//  Извлеките список в отдельный компонент
function Component({ topic, page, articles, onNextPage }) {
  return (
    <div>
      <h1>{topic}</h1>
      <ArticlesList articles={articles} />
      <div>Вы находитесь на странице {page}</div>
      <button onClick={onNextPage}>Дальше</button>
    </div>
  )
}

Пропы по умолчанию

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

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

//  Не определяйте значения пропов по умолчанию за пределами функции
function Component({ title, tags, subscribed }) {
  return <div>...</div>
}

Component.defaultProps = {
  title: '',
  tags: [],
  subscribed: false,
}

//  Поместите их в список аргументов
function Component({ title = '', tags = [], subscribed = false }) {
  return <div>...</div>
}

Вложенные функции рендеринга

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

Это означает, что вложенная функция будет иметь доступ к состоянию и данным внешней функции. Это делает код менее читаемым — что делает эта функция (за что она отвечает)?

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

//  Не вкладывайте одни компоненты в другие
function Component() {
  function renderHeader() {
    return <header>...</header>
  }
  return <div>{renderHeader()}</div>
}

//  Извлекайте их в отдельные компоненты
import Header from '@modules/common/components/Header'

function Component() {
  return (
    <div>
      <Header />
    </div>
  )
}

Управление состоянием


Редукторы

Порой нам требуется более мощный способ определения и управления состоянием, чем «useState()». Попробуйте использовать «useReducer()» перед обращением к сторонним библиотекам. Это отличный инструмент для управления сложным состоянием, не требующий использования зависимостей.

В комбинации с контекстом и TypeScript, «useReducer()» может быть очень мощным. К сожалению, его используют не очень часто. Люди предпочитают применять специальные библиотеки.

Если вам требуется несколько частей состояния, переместите их в редуктор:

//  Не используйте слишком много частей состояния
const TYPES = {
  SMALL: 'small',
  MEDIUM: 'medium',
  LARGE: 'large'
}

function Component() {
  const [isOpen, setIsOpen] = useState(false)
  const [type, setType] = useState(TYPES.LARGE)
  const [phone, setPhone] = useState('')
  const [email, setEmail] = useState('')
  const [error, setError] = useSatte(null)

  return (
    // ...
  )
}

//  Унифицируйте их с помощью редуктора
const TYPES = {
  SMALL: 'small',
  MEDIUM: 'medium',
  LARGE: 'large'
}

const initialState = {
  isOpen: false,
  type: TYPES.LARGE,
  phone: '',
  email: '',
  error: null
}

const reducer = (state, action) => {
  switch (action.type) {
    ...
    default:
      return state
  }
}

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

  return (
    // ...
  )
}

Хуки против HOC и рендер-пропов

В некоторых случаях нам требуется «усилить» компонент или предоставить ему доступ к внешним данным. Существует три способа это сделать — компоненты высшего порядка (HOC), рендеринг через пропы и хуки.

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

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

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

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

//  Не используйте рендер-пропы
function Component() {
  return (
    <>
      <Header />
        <Form>
          {({ values, setValue }) => (
            <input
              value={values.name}
              onChange={e => setValue('name', e.target.value)}
            />
            <input
              value={values.password}
              onChange={e => setValue('password', e.target.value)}
            />
          )}
        </Form>
      <Footer />
    </>
  )
}

//  Используйте хуки
function Component() {
  const [values, setValue] = useForm()

  return (
    <>
      <Header />
        <input
          value={values.name}
          onChange={e => setValue('name', e.target.value)}
        />
        <input
          value={values.password}
          onChange={e => setValue('password', e.target.value)}
        />
      <Footer />
    </>
  )
}

Библиотеки для получения данных

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

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

Работать с внешними данными еще легче, если вы используете GraphQL-клиент наподобие Apollo. Он реализует концепцию состояния клиента из коробки.

Библиотеки для управления состоянием

В подавляющем большинстве случаев нам для управления состоянием приложения не нужны никакие библиотеки. Они требуются лишь в очень больших приложениях с очень сложным состоянием. В таких ситуациях я использую одно из двух решений — Recoil или Redux.

Ментальные модели компонентов


Контейнер и представитель

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

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

Данная ментальная модель по факту описывает паттерн проектирования MVC для серверных приложений. Там она прекрасно работает.

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

Компоненты с состоянием и без

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

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

Например, компонент <Form/> должен содержать данные формы. Компонент <Input/> должен получать значения и вызывать коллбеки. Компонент <Button/> должен уведомлять форму о желании пользователя отправить данные на обработку и т.д.

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

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

Структура приложения


Группировка по маршруту/модулю

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

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

//  Не группируйте компоненты по деталям технической реализации
├── containers
|   ├── Dashboard.jsx
|   ├── Details.jsx
├── components
|   ├── Table.jsx
|   ├── Form.jsx
|   ├── Button.jsx
|   ├── Input.jsx
|   ├── Sidebar.jsx
|   ├── ItemCard.jsx

//  Группируйте их по модулю/домену
├── modules
|   ├── common
|   |   ├── components
|   |   |   ├── Button.jsx
|   |   |   ├── Input.jsx
|   ├── dashboard
|   |   ├── components
|   |   |   ├── Table.jsx
|   |   |   ├── Sidebar.jsx
|   ├── details
|   |   ├── components
|   |   |   ├── Form.jsx
|   |   |   ├── ItemCard.jsx

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

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

«Контейнерная архитектура» не является неправильной, но она не очень общая (абстрактная). Она не скажет тому, кто ее изучает ничего, кроме того, что для разработки приложения используется React.

Общие модули

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

Так вы будете видеть, какие общие компоненты используются в вашем приложении, даже без помощи Storybook. Это позволяет избежать дублирования кода. Вы же не хотите, чтобы каждый член вашей команды разрабатывал собственный вариант кнопки? К сожалению, это часто происходит из-за плохой архитектуры приложения.

Абсолютные пути

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

//  Не используйте относительные пути
import Input from '../../../modules/common/components/Input'

//  Абсолютный путь никогда не изменится
import Input from '@modules/common/components/Input'

Я использую префикс "@" в качестве индикатора внутреннего модуля, но я также видел примеры использования символа "~".

Оборачивание внешних компонентов

Старайтесь не импортировать слишком много сторонних компонентов напрямую. Создавая адаптер для таких компонентов мы можем при необходимости модифицировать их API. Также мы можем менять используемые библиотеки в одном месте.

Это относится как к библиотекам компонентов, таким как Semantic UI, так и к утилитам. Простейший способ заключается в повторном экспорте таких компонентов из общего модуля.

Компоненту не нужно знать, какую конкретно библиотеку мы используем.

//  Не импортируйте зависимости напрямую
import { Button } from 'semantic-ui-react'
import DatePicker from 'react-datepicker'

//  Повторно экспортируйте их из внутреннего модуля
import { Button, DatePicker } from '@modules/common/components'

Один компонент — одна директория

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

Хорошей практикой является создание файла «index.js» для повторного экспорта компонента. Это позволяет не изменять пути импорта и избежать дублирования названия компонента — «import Form from 'components/UserForm/UserForm'». Однако, не следует помещать код компонента в файл «index.js», поскольку это сделает невозможным поиск компонента по названию вкладки в редакторе кода.

//  Не размещайте все файлы в одном месте
├── components
    ├── Header.jsx
    ├── Header.scss
    ├── Header.test.jsx
    ├── Footer.jsx
    ├── Footer.scss
    ├── Footer.test.jsx

//  Помещайте их в собственные директории
├── components
    ├── Header
        ├── index.js
        ├── Header.jsx
        ├── Header.scss
        ├── Header.test.jsx
    ├── Footer
        ├── index.js
        ├── Footer.jsx
        ├── Footer.scss
        ├── Footer.test.jsx

Производительность


Преждевременная оптимизация

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

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

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

После определения проблем, устраните их в порядке влияния на производительность.

Размер сборки

Количество JavaScript, отправляемое браузеру, это ключевой фактор производительности приложения. Само приложение может быть очень быстрым, но об этом никто не узнает, если для его запуска придется предварительно загружать 4 Мб JavaScript.

Не стремитесь к одному «бандлу». Разделяйте приложение на уровне маршрутов и даже больше. Убедитесь, что отправляете браузеру минимальное количество кода.

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

Повторный рендеринг — коллбеки, массивы и объекты

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

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

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

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

Тестирование


Тестирование при помощи снимков

Однажды, я столкнулся с интересной проблемой при проведении snapshot-тестирования: сравнение «new Date()» без аргумента с текущей датой всегда возвращало «false».

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

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

Тестирование корректного рендеринга

Основная задача тестирования заключается в подтверждении того, что компонент работает, как ожидается. Убедитесь в том, что компонент возвращает правильную разметку как с «дефолтными», так и с переданными пропами.

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

Тестирование состояния и событий

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

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

Тестирование пограничных случаев

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

Это может означать передачу пустого массива для того, чтобы убедиться в том, что доступ к индексу не осуществляется без проверки. Это также может означать вызов ошибки в компоненте (например, в запросе к API) для проверки ее правильной обработки.

Интеграционное тестирование

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

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

Стилизация


CSS-в-JS

Это очень спорный вопрос. Лично я предпочитаю использовать библиотеки вроде Styled Components или Emotion, позволяющие писать стили в JavaScript. Одним файлом меньше. Не надо думать о таких вещах, как, например, названия классов.

Структурной единицей React является компонент, так что техника CSS-в-JS или, точнее, все-в-JS, на мой взгляд, является наиболее предпочтительной.

Обратите внимание: другие подходы к стилизации (SCSS, CSS-модули, библиотеки со стилями типа Tailwind) не являются неправильными, но я все же рекомендую использовать CSS-в-JS.

Стилизованные компоненты

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

Тем не менее, когда стилизованных компонентов очень много, имеет смысл вынести их в отдельный файл. Я видел использование такого подхода в некоторых открытых проектах вроде Spectrum.

Получение данных


Библиотеки для работы с данными

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

Использование такого подхода означает, что отслеживание статуса загрузки и обработка HTTP-ошибок возлагается на нас. Это приводит к «многословности» и большому количеству шаблоного кода.

Вместо этого, лучше использовать такие библиотеки как React Query и SWR. Они делают взаимодействие с сервером органической частью жизненного цикла компонента идиоматическим способом — с помощью хуков.

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

Благодарю за внимание и хорошего начала рабочей недели.

Комментарии 2

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

    Правильно). Разметки мало, надо свой DSL)

      0
      bind в классах? статья 2018 года?

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

      Самое читаемое