Источник 👀
- Выполняй статический анализ кода с помощью
ESLint
. Используй плагинeslint-plugin-react-hooks
для перехвата ошибок, связанных с неправильным использованием хуков:
Установка
npm i eslint-plugin-react-hooks --save-dev
# or
yarn add eslint-plugin-react-hooks --dev
Настройки eslint
{
"extends": [
// ...
"plugin:react-hooks/recommended"
]
}
Или:
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
При использовании create-react-app
данный плагин включается в проект автоматически.
- Включай строгий режим. На дворе 2021 год, в конце концов.
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
-
Не обманывай
React
о зависимостях. Исправляй предупреждения/ошибки, связанные с зависимостями, возникающие при использовании хуковuseEffect
,useCallback
иuseMemo
. Для обеспечения актуальности колбеков без выполнения лишних ререндеров можно использовать такой паттерн. -
Не забывай добавлять ключи при использовании
map
для рендеринга компонентов из массивов. -
Используй хуки только на верхнем уровне. Не вызывай их в циклах, условиях и вложенных функциях.
-
Разберись с тем, что означает предупреждение
Can't perform state update on unmounted component
(невозможно обновить состояние размонтированного компонента). -
Избегай появления белого экрана смерти путем использования предохранителей на разных уровнях приложения. Предохранители можно также использовать для отправки сообщений в сервис мониторинга ошибок, такой как
Sentry
- статья о том, как это сделать. -
Если в консоли инструментов разработчика в браузере имеются предупреждения или ошибки, на это есть причина!
-
Не забывай про
tree-shaking
. -
Prettier
(или похожие инструменты) избавляет от необходимости заботиться о единообразном форматировании кода за счет автоматизации этого процесса. -
TypeScript
сделает твою жизнь намного проще. -
Для открытых проектов настоятельно рекомендую использовать
Code Climate
. Автоматическое обнаружение "дурно пахнущего" кода мотивирует меня на борьбу с техническим долгом. -
Next.js
- отличный фреймворк.
"Лучший код - это его отсутствие. Каждая новая строка кода, которую ты написал, это код, который кому-то придется отлаживать, код, который кому-то придется читать и понимать, код, который кому-то придется поддерживать." - Jeff Atwood
"Одним из самых продуктивных дней в моей жизни был день, когда я просто удалил 1000 строк кода." - Eric S. Raymond
TL;DR
- Думай перед добавлением новой зависимости.
- Пиши код в стиле, характерном для
React
. - Не умничай! YAGNI
1.2.1. Думай перед добавлением новой зависимости.
Чем больше зависимостей в проекте, тем большее количество кода загружается браузером. Спроси себя, действительно ли ты используешь возможности, предоставляемые конкретной библиотекой?
Возможно, тебе это не нужно
-
Действительно ли тебе нужен
Redux
? Может быть. Но не забывай, чтоReact
- сам по себе отличный инструмент для управления состоянием. -
Действительно ли тебе нужен
Apollo Client
?Apollo Client
предоставляет большое количество прикольных "фич". Однако его использование существенно увеличивает размер сборки. Если в приложении используются фичи, которые не являются уникальными дляApollo Client
, попробуй заменить его на такие библиотеки, какSWR
илиreact-query
(https://react-query.tanstack.com/comparison) (или обойтись вообще без библиотек). -
Axios
?axios
- отличная библиотека, возможности которой нелегко реализовать с помощью нативногоfetch
. Но если единственной причиной использованияaxios
является более привлекательный интерфейс, реализуй обертку надfetch
- статья о том, как это сделать. -
Возможно, для "темизации" (режим
light
/dark
) тебе не нужен контекст. Попробуй использовать для этогопеременные CSS
. -
Возможно, тебе не нужен
JavaScript
. СовременныйCSS
- очень мощный инструмент. Вам не нуженJavaScript
.
1.2.2. Пиши код в стиле, характерном для React
.
- Избегай сложных условий и используй короткие вычисления.
- Вместо традиционных циклов (
for
,for in
,for of
и т.д.) используй цепочки из встроенных функций высшего порядка (map
,filter
,reduce
,find
,some
и др.). Но не забывай, что в некоторых случаях традиционные циклы могут быть более производительными.
1.2.3. Не умничай!
"Что произойдет с моим софтом в будущем? Возможно, случится то и это. Давайте сразу это реализуем, раз уж мы все равно разрабатываем эту часть приложения. Это позволит предвосхитить дальнейшее развитие проекта".
Никогда так не делай! Реализуй только те вещи, которые нужны в данный момент. Не считай себя Нострадамусом.
Если ты встретил код, который "дурно пахнет", исправь его. Если исправить его сложно или на это нет времени, хотя бы пометь его комментарием (FIXME
или TODO
) с краткой характеристикой проблемы. Пусть все об этом знают. Это покажет другим, что тебе не все равно, а также добросовестное отношение к тому, что ты делаешь.
Примеры "дурно пахнущего" кода:
- ❌ методы или функции принимают большое количество аргументов
- ❌ труднопостижимые логические проверки
- ❌ длинные строки кода, помещенные в одну строку
- ❌ синтаксически идентичный дублирующийся код (такой код может иметь разное форматирование)
- ❌ функции или методы, выполняющие много задач
- ❌ классы или компоненты, включающие много функций или методов
- ❌ сложная логика в одной функции или методе
- ❌ функции или методы с большим количеством инструкций
return
- ❌ код с одинаковой структурой (названия переменных могут отличаться)
"Дурно пахнущий" код не обязательно означает, что такой код нужно менять. Это лишь означает, что, скорее всего, ты мог бы реализовать аналогичный функционал лучше.
setState
(изuseState
) иdispatch
(изuseReducer
) не нужно помещать в массив зависимостей таких хуков, какuseEffect
иuseCallback
, посколькуReact
гарантирует их стабильность.
// ❌
const decrement = useCallback(() => setCount(count - 1), [count, setCount])
const decrement = useCallback(() => setCount(count - 1), [count])
// ✅
const decrement = useCallback(() => setCount((c) => c - 1), [])
- если
useMemo
илиuseCallback
не имеют зависимостей, скорее всего, ты неправильно их используешь
// ❌
const MyComponent = (props) => {
const fnToCall = useCallback((str) => `hello ${str || 'world'}`, [])
const aConstant = useMemo(() => ({ x: 1, y: 3 }), [])
// ...
}
// ✅
const A_CONSTANT = { x: 1, y: 3 }
const fnToCall = (str) => `hello ${str || 'world'}`
const MyComponent = (props) => {
// ...
}
- оборачивай кастомный контекст в хук: это не только сделает интерфейс лучше, но и позволит ограничиться 1 импортом
//
import { useContext } from 'react'
import { SomeContext } from './context'
export default function App() {
const somethingFromContext = useContext(SomeContext)
// ...
}
//
export function useSomeContext() {
const context = useContext(SomeContext)
if (!context) {
throw new Error('useSomeContext может использоваться только внутри SomeProvider')
}
}
import { useSomeContext } from './context'
export default function App() {
const somethingFromContext = useSomeContext()
// ...
}
- думай о том, как компонент будет использоваться, перед тем, как писать код
"Любой дурак может писать код, который понимает компьютер. Хорошие программисты пишут код, который понимают люди." - Martin Fowler
"Соотношение времени, которое мы тратим на чтение кода, и времени, которое мы тратим на написание кода, составляет 10 к 1. Мы постоянно читаем старый код перед тем, как писать новый. Поэтому если ты хочешь быстро писать код, быстро выполнять задачи, если хочешь, чтобы писать код было легко, пиши такой код, который будет легко читать." - Robert C. Martin
TL;DR
- Избегай сложного состояния за счет удаления лишнего.
- Передавай банан, а не гориллу, держащую банан, вместе с джунглями (старайся передавать примитивы в качестве пропов).
- Компоненты должны быть маленькими и простыми - принцип единственной ответственности!
- Дублирование лучше плохой абстракции (избегай преждевременных / неуместных обобщений).
- Решай проблему передачи пропов за счет композиции.
Context
не единственное решение проблемы распределения состояния. - Разделяй большие
useEffect
на несколько небольших.
- Извлекай логику в хуки и вспомогательные функции.
- Большой компонент лучше разделить на
logical
иpresentational
компоненты (не обязательно, зависит от ситуации). - Старайся, чтобы в зависимостях
useEffect
,useCallback
иuseMemo
находились только примитивы. - Зависимостей этих хуков не должно быть слишком много.
- Вместе нескольких
useState
лучше использовать одинuseReducer
, особенно в случае, когда некоторые значения состояния зависят от других значений или предыдущего состояния. Context
не должен быть глобальным для всего приложения.Он должен находиться максимально близко к компонентам, потребляющим контекст.
Пример 1
Задача
Отобразить следующую информацию о треугольнике:
- длину каждой стороны
- гипотенузу
- периметр
- площадь
Треугольник - это объект { a: number, b: number }
из API
. a
и b
- две короткие стороны правильного треугольника.
Плохое решение
const Triangle = () => {
const [triangleInfo, setTriangleInfo] = useState<{a: number, b: number} | null>(null)
const [hypotenuse, setHypotenuse] = useState<number | null>(null)
const [perimeter, setPerimeter] = useState<number | null>(null)
const [area, setArea] = useState<number | null>(null)
useEffect(() => {
fetchTriangle().then((t) => setTriangleInfo(t))
}, [])
useEffect(() => {
if (!triangleInfo) return
const { a, b } = triangleInfo
const h = computeHypotenuse(a, b)
setHypotenuse(h)
const _area = computeArea(a, b)
setArea(newArea)
const p = computePerimeter(a, b, h)
setPerimeter(p)
}, [triangleInfo])
if (!triangleInfo) return null
// рендеринг
}
Решение получше
const Triangle = () => {
const [triangleInfo, setTriangleInfo] = useState<{a: number, b: number} | null>(null)
useEffect(() => {
fetchTriangle().then((t) => setTriangle(t))
}, [])
if (!triangleInfo) return null
const { a, b } = triangleInfo
const h = computeHypotenuse(a, b)
const area = computeArea(a, b)
const p = computePerimeter(a, b, h)
// рендеринг
}
Пример 2
Задача
Разработать компонент, который будет делать следующее:
- получать список уникальных точек от
API
- предоставлять кнопку для сортировки точек по
x
илиy
(по возрастанию) - предоставлять кнопку для изменения
maxDistance
(каждый раз увеличивая его значение на10
, начальным значением должно быть100
) - отображать только те точки, которые находятся не дальше
maxDistance
от центра (0, 0
) - предположим, что список включает всего 100 точек, поэтому о производительности можно не беспокоиться (в случае большого количества элементов, можно мемоизировать некоторые вычисления с помощью
useMemo
)
Плохое решение
type SortBy = 'x' | 'y'
const toggle = (current: SortBy): SortBy => current === 'x' ? 'y' : 'x'
const Points = () => {
const [points, setPoints] = useState<{x: number, y: number}[]>([])
const [filteredPoints, setFilteredPoints] = useState<{x: number, y: number}[]>([])
const [sortedPoints, setSortedPoints] = useState<{x: number, y: number}[]>([])
const [maxDistance, setMaxDistance] = useState<number>(100)
const [sortBy, setSortBy] = useState<SortBy>('x')
useEffect(() => {
fetchPoints().then((r) => setPoints(r))
}, [])
useEffect(() => {
const sorted = sortPoints(points, sortBy)
setSortedPoints(sorted)
}, [points, sortBy])
useEffect(() => {
const filtered = sortedPoints.filter((p) => getDistance(p.x, p.y) < maxDistance)
setFilteredPoints(filtered)
}, [sortedPoints, maxDistance])
const otherSortBy = toggle(sortBy)
const pointsToDisplay = filteredPoints.map((p) => <li key={`${p.x}-${p.y}`}>({p.x}, {p.y})</li>)
return (
<>
<button onClick={() => setSortBy(otherSortBy)}>
Сортировать по: {otherSortBy}
<button>
<button onClick={() => setMaxDistance(maxDistance + 10)}>
Увеличить максимальную дистанцию
<button>
<p>Отображаются точки, которые находятся ближе, чем {maxDistance} от центра (0, 0).<br />
Точки отсортированы по: {sortBy} (по возрастанию)</p>
<ol>{pointToDisplay}</ol>
</>
)
}
Решение получше
// Обратите внимание: здесь также можно использовать `useReducer`
type SortBy = 'x' | 'y'
const toggle = (current: SortBy): SortBy => current === 'x' ? : 'y' : 'x'
const Points = () => {
const [points, setPoints] = useState<{x: number, y: number}[]>([])
const [maxDistance, setMaxDistance] = useState<number>(100)
const [sortBy, setSortBy] = useState<SortBy>('x')
useEffect(() => {
fetchPoints().then((r) => setPoints(r))
}, [])
const otherSortBy = toggle(sortBy)
const filteredPoints = points.filter((p) => getDistance(p.x, p.y) < maxDistance)
const pointToDisplay = sortPoints(filteredPoints, sortBy).map((p) => <li key={`${p.x}|${p.y}`}>({p.x}, {p.y})</li>)
return (
<>
<button onClick={() => setSortBy(otherSortBy)}>
Сортировать по: {otherSortBy}
<button>
<button onClick={() => setMaxDistance(maxDistance + 10)}>
Увеличить максимальную дистанцию
<button>
<p>Отображаются точки, которые находятся ближе, чем {maxDistance} от центра (0, 0).<br />
Точки отсортированы по: {sortBy} (по возрастанию)</p>
<ol>{pointToDisplay}</ol>
</>
)
}
"Хотел банан, а получил гориллу, держащую банан, вместе с джунглями." - Joe Armstrong
В качестве пропов должны передаваться примитивы (по-возможности).
Это сделает компоненты менее зависимыми друг от друга, степень зависимости компонентов будет ниже. Слабая связанность облегчает изменение, замену и удаление одних компонентов без необходимости серьезной модификации других.
Пример
Задача
Создать компонент MemberCard
, включающий 2 компонента: Summary
и SeeMore
. MemberCard
принимает проп id
. MemberCard
использует хук useMember
для получения следующей информации на основе id
:
type Member = {
id: string
firstName: string
lastName: string
title: string
imgUrl: string
webUrl: string
age: number
bio: string
// еще 100 полей
}
Компонент SeeMore
должен отображать bio
и age
member
, а также предоставлять кнопку для показа и скрытия этой информации.
Компонент Summary
должен отображать изображение member
. Также он должен отображать title
, firstName
и lastName
. Клик по имени member
должен перенаправлять на его профиль. Summary
также может иметь дополнительный функционал.
Плохое решение
const Summary = ({ member }: { member: Member }) => {
return (
<>
<img src={member.imgUrl} />
<a href={member.webUrl} >
{member.title}. {member.firstName} {member.lastName}
</a>
</>
)
}
const SeeMore = ({ member }: { member: Member }) => {
const [seeMore, setSeeMore] = useState<boolean>(false)
return (
<>
<button onClick={() => setSeeMore(!seeMore)}>
{seeMore ? "Скрыть" : "Показать"}
</button>
{seeMore && <>Возраст: {member.age} | Биография: {member.bio}</>}
</>
)
}
const MemberCard = ({ id }: { id: string }) => {
const member = useMember(id)
return <><Summary member={member} /><SeeMore member={member} /></>
}
Решение получше
const Summary = ({ imgUrl, webUrl, header }: { imgUrl: string, webUrl: string, header: string }) => {
return (
<>
<img src={imgUrl} />
<a href={webUrl}>{header}</a>
</>
)
}
const SeeMore = ({ componentToShow }: { componentToShow: ReactNode }) => {
const [seeMore, setSeeMore] = useState<boolean>(false)
return (
<>
<button onClick={() => setSeeMore(!seeMore)}>
{seeMore ? "Скрыть" : "Показать"}
</button>
{seeMore && <>{componentToShow}</>}
</>
)
}
const MemberCard = ({ id }: { id: string }) => {
const { title, firstName, lastName, webUrl, imgUrl, age, bio } = useMember(id)
const header = `${title}. ${firstName} ${lastName}`
return (
<>
<Summary {...{ imgUrl, webUrl, header }} />
<SeeMore componentToShow={<>Возраст: {age} | Биография: {bio}</>} />
</>
)
}
Что означает принцип единственной ответственности?
Это означает, что компонент должен решать только одну задачу.
Компонент, который решает несколько задач, сложно переиспользовать. Например, когда нам требуется лишь часть функционала такого компонента. Скорее всего, код такого компонента будет сложным и запутанным. Компоненты, которые решают одну задачу, изолированы от остального приложения, что позволяет произвольно комбинировать компоненты и переиспользовать их без дублирования.
Как понять, что компонент отвечает данному критерию?
Попробуй описать компонент одним предложением. Если компонент решает одну задачу, у тебя получится это сделать. Если же тебе приходится использовать союзы "и" или "или", тогда компонент, скорее всего, нарушает принцип единственной ответственности.
Исследуй состояние компонента, его пропы и хуки, которые в нем используются, а также переменные и методы, которые определяются в компоненте (их не должно быть слишком много). Нужны ли все эти вещи для решения компонентом поставленной перед ним задачи? Если какие-то вещи не нужны, перенеси их в другое место или раздели компонент на части.
Пример
Задача
"Нарисовать" несколько кнопок для покупки товаров определенных категорий. Например, пользователь может выбрать сумки, стулья или еду.
- Нажатие каждой кнопки приводит к открытию модального окна, где можно выбирать и "сохранять" товары
- При бронировании товара рядом с соответствующей кнопкой должна отображаться галочка
- Пользователь должен иметь возможность редактировать заказ (добавлять или удалять товары) даже после бронирования
- При наведении на кнопку курсора должен отображаться компонент
WavingHand
- Кнопка должна блокироваться при отсутствии товаров определенной категории
- При наведении курсора на заблокированную кнопку должна отображаться подсказка
Недоступно
- Фон заблокированной кнопки должен быть
gray
- Фон "забронированной" кнопки должен быть
green
- Фон обычной кнопки должен быть
red
- Кнопка для каждой категории должна иметь уникальную подпись и иконку
Плохое решение
type ShopCategoryTileProps = {
isBooked: boolean
icon: ReactNode
label: string
componentInsideModal?: ReactNode
items?: { name: string, quantity: number }[]
}
const ShopCategoryTile = ({
icon,
label,
items
componentInsideModal,
}: ShopCategoryTileProps ) => {
const [openDialog, setOpenDialog] = useState(false)
const [hover, setHover] = useState(false)
const disabled = !items || items.length === 0
return (
<>
<Tooltip title="Недоступно" show={disabled}>
<StyledButton
className={disabled ? "gray" : isBooked ? "green" : "red" }
disabled={disabled}
onClick={() => disabled ? null : setOpenDialog(true) }
onMouseEnter={() => disabled ? null : setHover(true)}
onMouseLeave={() => disabled ? null : setHover(false)}
>
{icon}
<StyledLabel>{label}<StyledLabel/>
{!disabled && isBooked && <FaCheckCircle/>}
{!disabled && hover && <WavingHand />}
</StyledButton>
</Tooltip>
{componentInsideModal &&
<Dialog open={openDialog} onClose={() => setOpenDialog(false)}>
{componentInsideModal}
</Dialog>
}
</>
)
}
Решение получше
// разделяем компонент на 2 штуки
const DisabledShopCategoryTile = ({ icon, label }: { icon: ReactNode, label: string }) => {
return (
<Tooltip title="Недоступно">
<StyledButton disabled={true} className="gray">
{icon}
<StyledLabel>{label}<StyledLabel/>
</Button>
</Tooltip>
)
}
type ShopCategoryTileProps = {
icon: ReactNode
label: string
isBooked: boolean
componentInsideModal: ReactNode
}
const ShopCategoryTile = ({
icon,
label,
isBooked,
componentInsideModal,
}: ShopCategoryTileProps ) => {
const [opened, setOpened] = useState(false)
const [hovered, setHovered] = useState(false)
const open = useCallback(() => setOpenDialog(!opened), [])
const hover = useCallback(() => setHover(!hovered), [])
return (
<>
<StyledButton
disabled={false}
className={isBooked ? "green" : "red"}
onClick={open}
onMouseEnter={hover}
onMouseLeave={hover}
>
{icon}
<StyledLabel>{label}<StyledLabel/>
{isBooked && <FaCheckCircle/>}
{hovered && <WavingHand />}
</StyledButton>
{opened &&
<Dialog onClose={open}>
{componentInsideModal}
</Dialog>
}
</>
)
}
Еще одно плохое решение из одного реального продакшна
const ShopCategoryTile = ({
item,
offers,
}: {
item: ItemMap;
offers?: Offer;
}) => {
const dispatch = useDispatch();
const location = useLocation();
const history = useHistory();
const { items } = useContext(OrderingFormContext)
const [openDialog, setOpenDialog] = useState(false)
const [hover, setHover] = useState(false)
const isBooked =
!item.disabled && !!items?.some((a: Item) => a.itemGroup === item.group)
const isDisabled = item.disabled || !offers
const RenderComponent = item.component
useEffect(() => {
if (openDialog && !location.pathname.includes("item")) {
setOpenDialog(false)
}
}, [location.pathname]);
const handleClose = useCallback(() => {
setOpenDialog(false)
history.goBack()
}, [])
return (
<GridStyled
xs={6}
sm={3}
md={2}
item
booked={isBooked}
disabled={isDisabled}
>
<Tooltip
title="Недоступно"
placement="top"
disableFocusListener={!isDisabled}
disableHoverListener={!isDisabled}
disableTouchListener={!isDisabled}
>
<PaperStyled
disabled={isDisabled}
elevation={isDisabled ? 0 : hover ? 6 : 2}
>
<Wrapper
onClick={() => {
if (isDisabled) {
return;
}
dispatch(push(ORDER__PATH));
setOpenDialog(true);
}}
disabled={isDisabled}
onMouseEnter={() => !isDisabled && setHover(true)}
onMouseLeave={() => !isDisabled && setHover(false)}
>
{item.icon}
<Typography variant="button">{item.label}</Typography>
<CheckIconWrapper>
{isBooked && <FaCheckCircle size="26" />}
</CheckIconWrapper>
</Wrapper>
</PaperStyled>
</Tooltip>
<Dialog fullScreen open={openDialog} onClose={handleClose}>
{RenderComponent && (
<RenderComponent item={item} offer={offers} onClose={handleClose} />
)}
</Dialog>
</GridStyled>
)
}
Избегайте преждевременной / неуместной генерализации. Если реализация простой фичи требует написания большого количества кода, изучите другие способы ее реализации. Советую взглянуть на The Wrong Abstraction.
"Преждевременная оптимизация - корень зла." - Tony Hoare
"Одно точное измерение лучше тысячи экспертных мнений." - Grace Hopper
TL;DR
- Если тебе кажется, что что-то работает медленно, убедись в этом с помощью специальных инструментов, например, профилировщика инструментов React-разработчика.
- Используй
useMemo
только для дорогих вычислений. - Используй
memo
,useMemo
иuseCallback
для предотвращение повторного рендеринга. При этом, количество зависимостей должно быть небольшим, а сами зависимости, преимущественно, должны быть примитивами. - Убедись, что
memo
,useMemo
иuseCallback
решают задачу (действительно ли они предотвращают ререндеринг?). - Почини медленный рендеринг перед тем, как чинить ререндеринг.
- Размещай состояние максимально близко к компонентам, которые его используют. Это облегчит изучение кода и сделает приложение быстрее (колокация или совместное размещение состояний).
Context
должен быть логически отделен от остальной части приложения. Через провайдер должно передаваться минимально возможное количество значений. Это объясняется тем, что компоненты, потребляющие контекст, будут подвергаться повторному рендерингу при изменении любого значения, содержащегося в контексте, даже если они не используют это значение (кажется, что эту проблему можно решить с помощьюuseMemo
).- Контекст можно оптимизировать посредством разделения
state
иdispatch
. - Разберись с ленивой загрузкой и разделением кода.
- "Виртуализируй" списки (с помощью
react-virtual
или похожих решений). - Меньший размер сборки, как правило, означает более быстрое приложение.
- Какую библиотеку для работы с формами использовать? Никакую. Но если очень хочется, то советую взглянуть на
react-hook-form
.
- State Colocation will make your React app faster
- When to useMemo and useCallback
- Fix the slow render before you fix the re-render
- Profile a React App for Performance
- How to optimize your context value
- How to use React Context effectively
- One React mistake that's slowing you down
- One simple trick to optimize React re-renders