- Компоненты
- Пропсы
- Иконки
- SVG
- Экспорт
- Переменные окружения
- Работа с API
- Общие принципы работы со стейт менеджерами
- Оптимизация
- Общее
В этом топике собраны 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.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 ( ... ); };
- 2.1 Пропсы компонента объявлять перед компонентом в том же файле. Это позволит не переключаться между файлами при чтении и беглом просмотре компонента.
- 2.2 Именовать пропсы компонента словом
Props
. Более подробное имя для пропсов не требуется, из контекста и так понятно, что это пропсы для текущего компонента.
- 2.3 Если ваш компонент предполагает обязательное использование пропа
children
, явно объявите его вProps
и сделайте обязательным. Еслиchildren
необязательный, можно воспользоваться вспомогательным встроенным типомPropsWithChildren<Props>
.
- 2.4 Не экспортируйте пропсы компонента без причины. Во первых это засоряет подсказки редактора кода, во вторых неясно, эти пропсы действительно где-то используются или нет. Экспортируйте только если они используются где-то ещё. При экспорте пропсам следует дать более осмысленное имя, добавив название компонента в начало:
Примечание: правило применимо, если вы пропсы объявляете в том же файле, что и сам компонент.
export { type Props as MyComponentProps };
-
2.5 Порядок указания пропсов должен быть следующим:
-
Все обязательные пропы-React компоненты
-
Все необязательные пропы-React компоненты
-
Все обязательные значения.
-
Все необязательные значения.
-
Все обязательные функции.
-
Все необязательные функции.
Все необязательные пропсы-значения должны иметь значение по умолчанию.
Если в проекте нет других соглашений, для типов функций используется синтаксис
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.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;
- использовать префетчинг(обычно, браузеры делают это за нас оптимальным образом: например загружают изображение только тогда, когда пользователь проскроллил до нужного места).
- 4.1 Во многих фреймворках встроен SVGR, который позволяет подключать svg-файлы как реакт-компоненты. Проблема в том, что у такого компонента напрочь отсутствует типизация, ему можно передать любые пропсы. Решить эту проблему можно следующим образом:
-
создайте файл
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; }
-
Добавьте путь к файлу в массив
include
в вашемtsconfig.json
(в примере файл находится в корне проекта): -
Далее при импорте svg - файла вам надо будет писать:
import { ReactComponent as FooImage } from './images/foo.svg'; import { ReactComponent as BarImage } from './images/bar.svg';
Но эту запись можно улучшить:
-
Создайте папку
<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 }
-
Создайте файл
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
.
- 7.1 Поместите функции по работе с API в папку
api
, структурируйте её. Будь-то обычныйfetch
или использованиеaxios
с его конфигом, интерсепторами, пр. Там же могут лежать и типы ответов на запросы. Затем вызывайте эти функции в стейт менеджере, компонентах, где либо ещё. Так у вас в одном месте будет вся работа с api. Вот пример одной из возможных структур с использованиемaxios
:Исключения: использование библиотек, предлагающих свою структуру организации работы с API. Например таких какapi ├── controllers │ ├── feedback │ │ ├── converters # папка с конвертерами для данных запросов │ │ ├── controller.ts # содержит функции get, post, delete для /feedback │ │ ├── index.ts # реэкспорт │ │ ├── types.ts # типы для данных запросов ├── config.ts # конфиг axios ├── core.ts # интерсепторы axios ├── helpers.ts
rtk-query
.
Если у вас в проекте нет явной архитектуры:
- 8.1 Cтарайтесь разделять стейт по функционалу. Не пихайте всё в один стейт. Если вы используете стейт менеджер для запросов к API, возможно вам поможет структура сущностей бэкенда - users, rooms, orders, auth.
- 8.2 Если вы используете стейт менеджер для запросов к API, делайте все запросы через стейт менеджер. Даже если они никак не взаимодействуют со стейтом (напр. это может быть POST-запрос, который ничего не возвращает). Нужно это для однообразия, что бы не было так, что часть компонентов делают запросы напрямую, а часть через диспатч.
- В интернете есть множество материалов про преждевременную оптимизацию и чем она вредна. Помните об этом.
- Если ваш код работает некорректно без оптимизации, а с оптимизацией проблема уходит, то вам стоит в первую очередь устранить проблему, и только потом оптимизировать.
- Мы же хотим поговорить про особенности рендеринга React приложений и частые кейсы, из-за которых ваше приложение может начать тормозить. У нас есть крайне полезный топик, посвящённый некоторым оптимизациям при разработке React-приложений.
- 10.1 Не создавайте не нужных
.tsx
файлов. Если в таком файле нет JSX кода, он должен иметь расширение.ts
.