Чистый код в React: практики, которые упрощают поддержку и развитие проекта

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

При этом эти практики не являются абсолютной догмой: их цель
— читаемость и предсказуемость кода, а не максимальное количество отдельно вынесенных компонентов и утилит.
Оценить материал
Остальные статьи по react