Чистый код на React: практики, которые делают проект поддерживаемым
- среда, 31 декабря 2025 г. в 00:00:04

В работе над React-проектами код почти всегда живёт дольше, чем кажется на старте: требования меняются, команда растёт, появляются новые сценарии и интеграции. В таких условиях выигрывает не тот, кто «быстрее собрал», а тот, кто оставил после себя понятную структуру — с предсказуемой логикой, прозрачными зависимостями и минимальным количеством скрытых допущений.
В данной статье мы расскажем о принципах «чистого кода» в React, которые используем в повседневной разработке, и покажем их на коротких примерах.
Когда компонент одновременно отвечает и за бизнес-логику экрана, и за отображение списка, он быстро разрастается: появляются map, проверки на пустые данные, условия для разных состояний, сортировка/фильтрация. В результате основной компонент становится перегруженным и сложнее читается.
Практичнее выделить рендер списка в отдельный компонент. Так основной компонент остаётся «контейнером» (получает данные, управляет состояниями), а список становится самостоятельным и переиспользуемым блоком интерфейса.
Почему это хорошо:
Читаемость и поддержка. Вся логика списка (рендер, пустые состояния, сортировка, условные элементы) сосредоточена в одном месте.
Переиспользование. Один и тот же компонент списка можно подключать на других экранах без копирования map и условий.
Чистый основной компонент. Он не захламляется циклом рендера и сопутствующими проверками — остаётся только структура страницы и передача данных.
Было:
import { Button } from '@/shared/components/button';
import { useDeviceFlags } from '@/shared/hooks';
import styles from './brandTabs.module.scss';
import { QuickFilters } from '../quickFilters';
export const BrandTabs = ({
brands,
activeBrandCode,
onBrandClick,
quickFilters,
activeQuickFilterCode,
onQuickFilterClick,
}) => {
const { isDesktop } = useDeviceFlags();
const hasBrands = brands.length > 0;
const hasQuickFilters = quickFilters.length > 0;
if (!hasBrands && !hasQuickFilters) {
return null;
}
return (
<div className={styles.container}>
{hasBrands && (
<div className={styles.brands}>
{brands.map(({ id, name, code }) => {
const isSelected = activeBrandCode === code;
const handleClick = () => {
onBrandClick(code);
};
return (
<Button
key={id}
type="button"
variant="tab"
isActive={isSelected}
onClick={handleClick}
className={styles.btn}
>
{name}
</Button>
);
})}
</div>
)}
{!isDesktop && hasQuickFilters && (
<QuickFilters
items={quickFilters}
activeCode={activeQuickFilterCode}
onClick={onQuickFilterClick}
/>
)}
</div>
);
};В этом виде рендер списка находится в одном компоненте с остальной логикой и разметкой, из-за чего компонент разрастается и становится сложнее для чтения и поддержки.
Стало:
const BrandButtonsList = ({
brands,
activeBrandCode,
onBrandClick,
}) => {
return (
<>
{brands.map(({ id, name, code }) => {
const isSelected = activeBrandCode === code;
const handleClick = () => {
onBrandClick(code);
};
return (
<Button
key={id}
type="button"
variant="tab"
isActive={isSelected}
onClick={handleClick}
className={styles.btn}
>
{name}
</Button>
);
})}
</>
);
};
export const BrandTabs = ({
brands,
activeBrandCode,
onBrandClick,
quickFilters,
activeQuickFilterCode,
onQuickFilterClick,
}) => {
const { isDesktop } = useDeviceFlags();
const hasBrands = brands.length > 0;
const hasQuickFilters = quickFilters.length > 0;
if (!hasBrands && !hasQuickFilters) {
return null;
}
return (
<div className={styles.container}>
{hasBrands && (
<div className={styles.brands}>
<BrandButtonsList
brands={brands}
activeBrandCode={activeBrandCode}
onBrandClick={onBrandClick}
/>
</div>
)}
{!isDesktop && hasQuickFilters && (
<QuickFilters
items={quickFilters}
activeCode={activeQuickFilterCode}
onClick={onQuickFilterClick}
/>
)}
</div>
);
};Теперь логика рендера списка вынесена в отдельный компонент ItemsList, а в Component осталась только базовая верстка и передача данных.
Если функция не зависит от состояния, пропсов или жизненного цикла компонента (например, форматирование дат, сортировка массивов, преобразование строк), её лучше вынести в отдельный модуль. Компонент должен отвечать за отображение и работу со своим состоянием, а вспомогательная логика — жить отдельно.
Почему это хорошо:
Компонент проще читать и поддерживать. Внутри остаётся только то, что относится к UI и состояниям.
Логику проще переиспользовать. Одна и та же функция может применяться в разных компонентах без дублирования.
Проще тестировать. Утилиту можно проверять отдельно, не затрагивая рендер компонента.
Было:
export const OrderDate = ({ date }) => {
const formatOrderDate = (value) => {
const options = { year: 'numeric', month: 'long', day: 'numeric' };
return new Date(value).toLocaleDateString('ru-RU', options);
};
const formattedDate = formatOrderDate(date);
return <div>{formattedDate}</div>;
};Здесь formatOrderDate объявлена внутри компонента. Со временем такие функции накапливаются, перегружают компонент и усложняют повторное использование этой логики в других местах.
Стало:
const RU_DATE_FORMATTER = new Intl.DateTimeFormat('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
export const formatOrderDate = (date) => {
return RU_DATE_FORMATTER.format(new Date(date));
};import { formatOrderDate } from '../utils/dateUtils';
export const OrderDate = ({ date }) => {
const formattedDate = formatOrderDate(date);
return <div>{formattedDate}</div>;
};Теперь функция находится в утилитах: компонент стал компактнее, а форматирование даты можно использовать повторно и тестировать независимо от UI.
Если компоненту или функции требуется только часть данных из большого объекта, лучше передавать именно нужные поля, а не весь объект целиком. Так интерфейс компонента становится прозрачнее: по сигнатуре сразу видно, какие данные действительно используются, и от чего компонент зависит.
Почему это хорошо:
Код читается проще. Не нужно «пробираться» через объект и искать, какие поля реально используются.
Поддержка и тестирование легче. Зависимости явные: при изменениях сразу понятно, что может затронуть компонент.
Меньше лишних связей. Компонент не привязан к полной структуре объекта и не «тянет» за собой ненужные данные.
Было:
const UserInfo = ({ user }) => {
return (
<div>
<p>Имя: {user.name}</p>
<p>Возраст: {user.age}</p>
</div>
);
};
export const UserProfile = ({ user }) => {
return (
<section>
<h2>Профиль</h2>
<UserInfo user={user} />
</section>
);
};
Здесь в UserInfo передаётся весь объект user, хотя используются только два поля из всего объекта: name и age. Это делает компонент зависимым от структуры объекта и усложняет изменения.
Стало:
const userName = userObject.name
const userAge = userObject.age
<UserInfo name={userName} age={userAge} />
const UserInfo = ({ name, age }: UserInfoProps )=> {
return (
<div>
<p>Имя: {name}</p>
<p>Возраст: {age}</p>
</div>
);
}Теперь компонент явно декларирует необходимые данные и не зависит от полного объекта user, что упрощает сопровождение и снижает риск побочных изменений.
Если условие состоит из нескольких логических операторов и начинает «раздувать» код, его стоит вынести в отдельную константу с понятным названием. Это делает логику более очевидной: вместо чтения сложного выражения в строку вы читаете смысл условия.
Почему это хорошо:
Выше читаемость. Код легче воспринимается, особенно в хуках и JSX.
Проще отладка и изменения. Условие можно быстро проверить, переиспользовать или расширить, не перегружая основной блок.
Смысл в названии. Именованная константа сразу объясняет, за что отвечает проверка.
Было:
useEffect(() => {
if (isInitialLoad && !allMessages.length && isNewMessageReceived) {
return;
}
scrollToBottom(scrollableDivRef, 'auto');
setIsInitialLoad(false);
}, [isInitialLoad, allMessages, isNewMessageReceived]);Здесь условие «растворяется» внутри useEffect: с первого взгляда сложно понять, что именно проверяется и почему при выполнении условия происходит return.
Стало:
useEffect(() => {
const shouldSkipScroll = isInitialLoad && allMessages.length === 0 && isNewMessageReceived;
if (shouldSkipScroll) {
return;
}
scrollToBottom(scrollableDivRef, 'auto');
setIsInitialLoad(false);
}, [isInitialLoad, allMessages.length, isNewMessageReceived]);Теперь проверка вынесена в отдельную константу: код читается быстрее, а название shouldScrollOnInitialLoad сразу фиксирует смысл условия.
При работе с вложенными объектами часто появляются длинные цепочки вида a.b.c.d, а вместе с ними — проверки на существование каждого уровня. Если оставить это прямо в JSX или в условиях, код становится тяжёлым для восприятия. Практичнее вынести доступ к данным в промежуточные константы и использовать безопасное обращение к полям.
Почему это хорошо:
Меньше «шума» в JSX и условиях. Разметка остаётся простой, без цепочек и лишних проверок.
Проще менять структуру данных. Если вложенность изменится, правки будут локализованы в одном месте.
Ниже риск ошибок. Логика доступа к данным становится очевиднее и аккуратнее.
Было:
const VehicleInfo = ({ techniqueCard }) => {
return (
<div>
{techniqueCard.specialVehicle && techniqueCard.specialVehicle.model
? techniqueCard.specialVehicle.model.data
: 'Нет данных'}
</div>
);
};Здесь доступ к данным и проверки на существование вложенных полей находятся прямо в JSX, из-за чего разметка перегружается и читается хуже.
Стало:
const VehicleInfo = ({ techniqueCard }) => {
const specialVehicle = techniqueCard?.specialVehicle;
const modelData = specialVehicle?.model?.data;
const modelToDisplay = modelData ?? 'Нет данных';
return <div>{modelToDisplay}</div>;
};Теперь получение данных вынесено в константы: JSX стал чище, а логика доступа к вложенным полям — короче и понятнее.
«Магические числа» — это значения, которые встречаются в коде без контекста: непонятно, почему выбран именно этот порог, процент или лимит, и что он означает с точки зрения бизнес-логики. Корректнее выносить такие значения в константы с говорящими именами — тогда код становится самодокументируемым.
Почему это хорошо:
Понятнее при чтении. Не приходится разбираться, что означает 1000, 0.15, 300 или 15 и откуда они взялись.
Проще менять. Достаточно поправить значение в одном месте, без поиска по проекту.
Меньше ошибок. Снижается риск случайно использовать «не то число» или забыть обновить его в одном из участков кода.
Было:
const calculatePrice = (price) => {
if (price > 1000) {
return price - price * 0.15;
}
return price;
};Стало:
const DISCOUNT_THRESHOLD = 1000;
const DISCOUNT_RATE = 0.15;
const calculatePrice = (price) => {
if (price > DISCOUNT_THRESHOLD) {
return price - price * DISCOUNT_RATE;
}
return price;
};Теперь по именам констант сразу видно, что это за значения и какую роль они играют в логике расчёта.
В итоге эти 6 принципов помогают держать React-код в порядке: компоненты не разрастаются, логика не смешивается с разметкой, зависимости становятся очевидными, а числа и условия — понятными. Такой код проще читать, быстрее менять и спокойнее поддерживать, особенно когда проект растёт и над ним работает несколько человек.
При этом эти практики не являются абсолютной догмой: их цель — читаемость и предсказуемость кода, а не максимальное количество отдельно вынесенных компонентов и утилит.