Skip to content

Latest commit

 

History

History
468 lines (342 loc) · 28.1 KB

README.md

File metadata and controls

468 lines (342 loc) · 28.1 KB

React

  1. Компоненты
  2. Пропсы
  3. Иконки
  4. SVG
  5. Экспорт
  6. Переменные окружения
  7. Работа с API
  8. Общие принципы работы со стейт менеджерами
  9. Оптимизация
  10. Общее

Интро

В этом топике собраны best practices, как напрямую касающиеся React, так и те, которые так или иначе затрагивают будни frontend-разработчика. Помимо самих best practices, в этом топике также содержатся правила, относящиеся больше к стайлгайду, нежели к лучшим практикам. Таким образом хотелось бы прийти к большему однообразию в написании кода на разных проектах, что упростит переход разработчика с одного проекта на другой, поскольку при чтении и анализе кода/структуры проекта будет прилагаться меньше когнитивных усилий. Также это может уменьшить количество холиваров по поводу разных стилей написания кода (но это неточно :). Следует не забывать, что не стоит впадать в крайности. Иногда какое-то правило может только мешать и делать хуже, так как всех кейсов не опишешь.

Так же не забываем про топик JS, в котором много полезных правил вне зависимости от фреймворков.

Компоненты

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

    Плохо:

    const [isModalOpen, openModal] = useState();

    Хорошо:

    const [isModalOpen, setIsModalOpen] = useState();

  • 1.2 Разделяйте хуки useEffect для разных задач - React docs

  • 1.3 Помещайте вызовы useEffect в самый низ, перед return (есть исключение, описанное в конце 1.4).

  • 1.4 Не создавайте лишние JSX переменные. В подавляющем большинстве случаев их содержимое можно вставить внутрь оператора return.

    Плохо:

    const MyComponent: FC<Props> = ({ array, condition }) => {
      const list = array.map((item) => <li key={item}>{item}</li>);
    
      const conditionalRender = condition ? <div /> : <section />;
    
      return (
        <div>
          <ul>{list}</ul>
          {conditionalRender}
        </div>
      );
    };

    Хорошо:

    const MyComponent: FC<Props> = ({ array, condition }) => {
      return (
        <div>
          <ul>
            {array.map((item) => (
              <li key={item}>{item}</li>
            ))}
          </ul>
          {condition ? <div /> : <section />}
        </div>
      );
    };

    В редких случаях, когда ваш рендер зависит от сложного условия или цепочки условий, что приводит к плохой читабельности или вложенным условным операторам ?, лучше вынести такой код в функцию, в которой описать логику c использованием if. И затем вызвыть эту функцию в return. Такие функции должны быть в самом низу перед return, ниже чем вызовы useEffect (исключение для 1.3).

  • 1.5 Не объявляйте константы и функции-хелперы внутри компонента. Код компонента выполняется на каждом рендере, следовательно это лишние вычисления по созданию новых переменных на каждом рендере. Выносите их за пределы компонента, создавая файлы (напр. constants/helpers/utils/helperName) рядом с самим компонентом, если они специфичны для него или в более общее место (обычно это папка shared), если они могут использоваться где-то ещё.

    Плохо:

    const MyComponent: FC<Props> = ({ ... }) => {
      const DAYS_IN_WEEK = 7;
    
      const calculateSomeValue = () => { ... };
    
      return ( ... );
    };

    Хорошо:

    // Константы здесь рядом с компонентом для примера, их надо вынести в отдельные файлы.
    const DAYS_IN_WEEK = 7;
    
    const calculateSomeValue = () => { ... };
    
    const MyComponent: FC<Props> = ({ ... }) => {
      return ( ... );
    };

  • 1.6 Не забываем про хорошие практики компонентного подхода, описанные в разделе CSS 4.4-4.6

Пропсы

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

  • 2.2 Именовать пропсы компонента словом Props. Более подробное имя для пропсов не требуется, из контекста и так понятно, что это пропсы для текущего компонента.

  • 2.3 Если ваш компонент предполагает обязательное использование пропа children, явно объявите его в Props и сделайте обязательным. Если children необязательный, можно воспользоваться вспомогательным встроенным типом PropsWithChildren<Props>.

  • 2.4 Не экспортируйте пропсы компонента без причины. Во первых это засоряет подсказки редактора кода, во вторых неясно, эти пропсы действительно где-то используются или нет. Экспортируйте только если они используются где-то ещё. При экспорте пропсам следует дать более осмысленное имя, добавив название компонента в начало:
    export { type Props as MyComponentProps };
    Примечание: правило применимо, если вы пропсы объявляете в том же файле, что и сам компонент.

  • 2.5 Порядок указания пропсов должен быть следующим:

    1. Все обязательные пропы-React компоненты

    2. Все необязательные пропы-React компоненты

    3. Все обязательные значения.

    4. Все необязательные значения.

    5. Все обязательные функции.

    6. Все необязательные функции.

    Все необязательные пропсы-значения должны иметь значение по умолчанию.

    Если в проекте нет других соглашений, для типов функций используется синтаксис func(): type, а не func: () => type.

    Пример:

    type Props = {
      Content: FC;
      Icon?: FC;
      size: 'm' | 'l';
      color?: 'primary' | 'secondary' | 'inherit';
      useIsActive(): boolean;
      onClick?(): void;
    }
    
    const MyComponent: FC<Props> = ({ Content, Icon, size, color = 'inherit', useIsActive, onClick }) => {...}

  • 2.6 Пропсы компонента, относящиеся к обработке событий, должны именоваться по шаблону onEventName, где EventName — имя обрабатываемого события.

Иконки

Данный раздел основан на докладе Петра Жемчугова с HolyJS.

Существует множество способов отобразить иконку на странице, каждый из них имеет свои преимущества и недостатки. Наиболее оптимальным же способом на данный момент является использование CSS свойства mask-image:

  • 3.1 Реализация компонента
type Icon24Props = {
  name: "arrow-left" | 'check';
  size: 24;
}
type Icon16Props = {
  name: "arrow-down" | 'arrow-up' | 'check';
  size: 16;
}
// TIP: необходимость разделить иконки по размерам вызвана тем, что хоть svg и масштабируется, но делает оно это линейно: при изменении размеров какие-то линии уже изменили размер, какие-то еще нет -> полоска в 1px при сужении или плохо детализированное изображение при расширении будут плыть и выглядеть не очень хорошо, особенно на retina мониторах.
type Props = Icon24Props | Icon16Props;

// TIP: если у вас нет строгой дизайн-системы с фиксированным набором иконок фиксированного размера, то вы можете регулировать конечные размеры во внешних компонентах. Данный подход является плохой практикой так как это значительно ухудшает поддержку кода из-за отсутствия прозрачного набора состояний компонента, но в реальной жизни может рассматриваться как вариант к применению, но только с пониманием ограничений(качество отображения) и дальнейших проблем(усложняется поддержка). Задавать size просто number не рекомендуется так как это нарушит типизацию иконок с фиксированным размером и сделает значение еще менее конкретным.
// type IconStretchProps = {
//   name: Icon16Props['name'] | Icon24Props['name'];
//   size: 'stretch' // или другое название, которые считаете более точным
// }

const Icon: FC<Props> = ({ name, size }) => {
  const style = { maskImage: `url(https://<some-cdn-url>/icons/${name}-${size}.svg)` };

  return (
    <i className={`icon icon_size_${size}`} style={style} />
  )
}
.icon {
  display: inline-block;
  // TIP: может быть и градиентом при необходимости
  background: currentColor;
  mask-position: center center;

  &_size {
    &_16 {
      width: 16px;
      height: 16px;
    }

    &_24 {
      width: 24px;
      height: 24px;
    }

    &_stretch {
      width: 100%;
      height: 100%;
    }
  }
}

  • 3.2 Использование с другими компонентами:
// Лучше чем <Button leftIcon={<IconHeader24/>} /> или <Button LeftIcon={Button.IconHeader24} /> так как не требует создания компонента-обертки вида const IconHeader24 = () => { return <Icon size="heart" size={24} /> } и не позволяет пробросить недокументированный компонент. Размер иконки в данном случае выбирается в самом компоненте Button и наличие файла иконки с нужным размером гарантируется на уровне типов в компоненте Icon.
<Button leftIcon="heart" />

  • 3.3 Весь процесс выгрузки иконок в проект можно автоматизировать, как.

  • 3.4 Подводные камни:
    • mask-image, в отличии от простого src, подвержен CORS, решение: указать на сервере, на котором хранятся ваши картинки, следующие заголовки:
      • access-control-allow-origin: <ваш домен>
      • Access-Control-Max-Age: <время кеширования, например 86400>
    • кеширование, можно использовать одну из стратегий:
      • с версионированием иконок:
        • добавить версию в имя иконки: в само название или как параметр в url
        • указать заголовок Cache-Control: max-age=31536000, immutable, чтобы предотвратить проверку на 100 лет вперед
      • без версионирования иконок, - указать заголовки:
        • Cache-Control: max-age=86400, must-revalidate
        • ETag "<некоторый хеш>"
    • до появления иконки проходит некоторое время пока изображение не скачено, такое поведение может быть иногда нежелательным, например для показа лоадера, возможные решения:
      • если иконка используется повсюду на сайте, - скачивать всегда. Не подходит как вариант по умолчанию так как будет увеличиваться время time to interact;
      • использовать префетчинг(обычно, браузеры делают это за нас оптимальным образом: например загружают изображение только тогда, когда пользователь проскроллил до нужного места).

SVG

  • 4.1 Во многих фреймворках встроен SVGR, который позволяет подключать svg-файлы как реакт-компоненты. Проблема в том, что у такого компонента напрочь отсутствует типизация, ему можно передать любые пропсы. Решить эту проблему можно следующим образом:
    1. создайте файл global.d.ts. со следующим содержимым:

        declare module '*.svg' {
        import React from 'react';
      
        export const ReactComponent: React.FC<
          React.SVGProps<SVGSVGElement> & { title?: string }
        >;
      
        const src: string;
        export default src;
      }
    2. Добавьте путь к файлу в массив include в вашем tsconfig.json (в примере файл находится в корне проекта): image

    3. Далее при импорте svg - файла вам надо будет писать:

        import { ReactComponent as FooImage } from './images/foo.svg';
        import { ReactComponent as BarImage } from './images/bar.svg';

      Но эту запись можно улучшить:

      1. Создайте папку <Foo> в директории images и внутри поместите файл index.ts примерно следующего содержания:

        import { 
          default as normalSrc,
          ReactComponent as NormalComponent,
        } from './default.svg';
        import { 
          default as largerSrc,
          ReactComponent as LargerComponent,
        } from './larger.svg';
        import {
          default as smallerSrc,
          ReactComponent as SmallerComponent,
        } from './smaller.svg';
        
        // TIP: должно импортироваться из общего места хранения типов
        type Icon =  {
          src: string;
          Component: React.FC<
            React.SVGProps<SVGSVGElement> & { title?: string }
          >;
        };
        
        const normal: Icon = {
          src: normalSrc,
          Component: NormalComponent,
        };
        
        const larger: Icon = {
          src: largerSrc,
          Component: LargerComponent,
        };
        
        const smaller: Icon = {
          src: smallerSrc,
          Component: SmallerComponent,
        };
        
        export { normal, larger, smaller }
      2. Создайте файл index.ts в папке images и сделайте там реэкспорт ваших иконок-модулей:

          export * as FooImage from './Foo';

      Теперь у вас будет красивый импорт и типизированные svg:

        import { FooImage } from './images';
      
        <FooImage.larger.Component />
        <img src={FooImage.smaller.src} />

Экспорт

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

  • 5.2 Используйте реэкспорт. Например в корне папки components создайте index.ts файл и экспортируйте в нём все необходимые компоненты:

    // src/components/index.ts
    
    // Если вы используйете именованный экспорт
    export { MyComponent } from './MyComponent';
    // Если вы используете дефолтный экспорт
    export { default as MyComponent} from './MyComponent';

    Это позволит при импорте делать запись следующего вида:

    // Было
    import { MyComponent1 } from 'shared/components/MyComponent1';
    import { MyComponent2 } from 'shared/components/MyComponent2';
    import { MyComponent3 } from 'shared/components/MyComponent3';
    // Стало
    import { MyComponent1, MyComponent2, MyComponent3 } from 'shared/components';

    Во первых в большинстве случаев это сокращает запись импортов. Во вторых так проще следить за порядком импортов, так как у вас теперь меньше вложенности. В третьих вы явно декларируете, что можно импортировать из той или иной папки. У вас появляется "точка входа". Не всегда все компоненты, утилиты и т.д. должны экспортироваться наружу. Часть из них может быть предназначена для внутреннего пользования. Типичные случаи для реэкспорта: папки components, icons, utils, hooks, features/featureName, и т.д.

    Если вам нужно просто прокинуть реэкспорт из вложенной папки (со своим index.ts) на уровень выше, можно воспользоваться синтаксисом:

    export * from './folderName';

Переменные окружения

  • 6.1 Если в вашем проекте используются переменные окружения, например файл .env, создайте файл .env.example, который попадёт в индексацию git. Перечислите в нём все переменные окружения, но без их значений. Так будет понятно, какое ожидается содержимое .env файла для работы проекта. Для большей ясности можно указать тип переменной (не забывая, что все переменные окружения изначально строки), например:

    NEXT_PUBLIC_API_URL=string
    NEXT_PUBLIC_IS_TESTNET=boolean
    NEXT_PUBLIC_TRANSACTION_FEE=number
    
  • 6.2 Вынесите переменные окружения в отдельный объект, например:

    // shared/constants/environment.ts
    export const environment = {
      apiUrl: process.env.NEXT_PUBLIC_API_URL,
      isTestnet: process.env.NEXT_PUBLIC_IS_TESTNET,
      transactionFee: process.env.NEXT_PUBLIC_TRANSACTION_FEE
    };

    По умолчанию process.env никак не типизирован и даёт возможность обратиться к чему угодно. Это неудобно при обращении к переменным окружения. Приходится либо писать переменную по памяти (где легко ошибиться), либо копировать - вставить. Обращаясь к переменным окружения только в одном месте, вы сужаете все потенциальные ошибки до этого места. Там же можно выполнить предварительную обработку при необходимости, сделать приведение типа один раз вместо того, что бы делать это везде, где используется переменная. Так же запись вида environment.apiUrl куда приятнее, чем process.env.NEXT_PUBLIC_API_URL.

    В качестве альтернативы, если вы не хотите создавать отдельный объект, можно типизировать сам process.env.

Работа с API

  • 7.1 Поместите функции по работе с API в папку api, структурируйте её. Будь-то обычный fetch или использование axios с его конфигом, интерсепторами, пр. Там же могут лежать и типы ответов на запросы. Затем вызывайте эти функции в стейт менеджере, компонентах, где либо ещё. Так у вас в одном месте будет вся работа с api. Вот пример одной из возможных структур с использованием axios:
    api
    ├── controllers
    │   ├── feedback             
    │   │   ├── converters    # папка с конвертерами для данных запросов
    │   │   ├── controller.ts # содержит функции get, post, delete для /feedback   
    │   │   ├── index.ts      # реэкспорт
    │   │   ├── types.ts      # типы для данных запросов     
    ├── config.ts     # конфиг axios
    ├── core.ts       # интерсепторы axios
    ├── helpers.ts 
    
    Исключения: использование библиотек, предлагающих свою структуру организации работы с API. Например таких как rtk-query.

Общие принципы работы со стейт менеджерами

Если у вас в проекте нет явной архитектуры:

  • 8.1 Cтарайтесь разделять стейт по функционалу. Не пихайте всё в один стейт. Если вы используете стейт менеджер для запросов к API, возможно вам поможет структура сущностей бэкенда - users, rooms, orders, auth.

  • 8.2 Если вы используете стейт менеджер для запросов к API, делайте все запросы через стейт менеджер. Даже если они никак не взаимодействуют со стейтом (напр. это может быть POST-запрос, который ничего не возвращает). Нужно это для однообразия, что бы не было так, что часть компонентов делают запросы напрямую, а часть через диспатч.

Оптимизация

  • В интернете есть множество материалов про преждевременную оптимизацию и чем она вредна. Помните об этом.
  • Если ваш код работает некорректно без оптимизации, а с оптимизацией проблема уходит, то вам стоит в первую очередь устранить проблему, и только потом оптимизировать.
  • Мы же хотим поговорить про особенности рендеринга React приложений и частые кейсы, из-за которых ваше приложение может начать тормозить. У нас есть крайне полезный топик, посвящённый некоторым оптимизациям при разработке React-приложений.

Общее

  • 10.1 Не создавайте не нужных .tsx файлов. Если в таком файле нет JSX кода, он должен иметь расширение .ts.