
В работе над 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-код в порядке: компоненты не разрастаются, логика не смешивается с разметкой, зависимости становятся очевидными, а числа и условия — понятными. Такой код проще читать, быстрее менять и спокойнее поддерживать, особенно когда проект растёт и над ним работает несколько человек.
При этом эти практики не являются абсолютной догмой: их цель — читаемость и предсказуемость кода, а не максимальное количество отдельно вынесенных компонентов и утилит.
