Разработка · 8 мин чтения

React-компоненты для лендинга: что выносить, а что нет

React-компоненты для лендинга я выделяю не по правилу «один блок - один файл», а по тому, что реально экономит время на правках. За несколько лет сборки лендингов на React и Next.js для клиентов сложилась чёткая граница: что стоит выносить в отдельный компонент, что можно оставить прямо внутри страницы, а что там просто добавит лишний уровень вложенности без пользы. Ниже разбор по блокам, отдельно про форму заявки и кнопки, структура папок с типизацией пропсов и момент, когда декомпозиция начинает мешать, а не помогать.

Зачем вообще резать лендинг на React-компоненты

Технически весь лендинг помещается в один файл page.tsx на 500-700 строк, и он даже будет работать. Проблема вылезает на второй итерации правок. Клиент просит поменять местами блок с кейсами и блок с ценами - и в одном файле это значит скроллить полотно кода и держать в голове, где заканчивается один JSX-блок и начинается другой. Маркетолог хочет обновить текст в блоке с преимуществами, не трогая форму заявки, а верстальщик в это же время правит Hero под новый баннер. Если весь лендинг лежит в одном компоненте, эти правки конфликтуют друг с другом при мерже, даже когда физически они не пересекаются.

Разбивка на компоненты по секциям снимает это: каждый блок - отдельный файл, отдельная зона ответственности, отдельный git-diff. На практике это ускоряет ревью правок и даёт возможность быстро менять порядок секций на странице - переставить импорты в page.tsx проще, чем резать JSX внутри одного огромного файла. Плюс когда блок вынесен отдельно, его проще показать клиенту изолированно, в Storybook или просто на отдельном тестовом роуте, не поднимая всю страницу целиком.

Отдельные компоненты вдобавок проще тестировать - юнит-тест на Pricing, который проверяет, что подсвечивается нужный тариф, не должен рендерить весь лендинг целиком со всеми запросами формы.

Какие блоки лендинга стоит выносить в компоненты

Стандартный лендинг у меня почти всегда распадается на один и тот же набор секций, и каждая из них - кандидат на отдельный компонент, даже если используется один раз на странице:

Блок Что внутри Почему отдельный компонент
Header / Nav Логотип, меню, кнопка CTA Часто переиспользуется без изменений на других страницах сайта
Hero Заголовок, подзаголовок, главный CTA Меняется чаще всего под A/B‑тесты, должен обновляться изолированно
Advantages / Features Список карточек с иконками Данные легко вынести в массив и рендерить через map
Pricing Тарифы, таблица цен Логика подсветки «популярного» тарифа не должна лезть в другие блоки
Testimonials / Cases Отзывы, кейсы, слайдер Часто тянет за собой отдельную библиотеку слайдера, лучше изолировать
CTA-форма Форма заявки с валидацией и отправкой Единственный блок с настоящей бизнес-логикой на странице
Footer Контакты, ссылки, реквизиты Почти никогда не меняется, переиспользуется между проектами

Header и Footer я обычно вообще не трогаю между проектами - беру заготовку с прошлого лендинга на Next.js и адаптирую под бренд-цвета. Hero и Pricing переписываю каждый раз с нуля, потому что там завязана конкретика заказчика. Advantages и Testimonials почти всегда рендерятся из массива данных, и тут смысла плодить JSX-разметку для каждой карточки вручную нет - один компонент карточки плюс map по массиву закрывает всю секцию.

Отдельный момент - не сливать соседние секции в один компонент только потому, что визуально они идут подряд. Hero и Advantages выглядят как один экран на макете, но у них разная частота изменений и разные данные, и если слить их в один файл, при следующей правке Hero придётся снова резать JSX, от которого только что избавились.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Форма заявки и кнопки: где граница между UI-kit и бизнес-логикой

Кнопка - это просто UI

Кнопка на лендинге - чистый презентационный компонент: принимает variant (primary/secondary), size, children и обработчик onClick. Внутри нет ни запроса к API, ни валидации, только вёрстка и стили. Такие компоненты я выношу в components/ui и переиспользую буквально везде, от Hero до Footer. На части лендингов кнопка CTA не открывает форму, а сразу ведёт на оплату, например через Т‑Банк, если продукт продаётся напрямую с одной страницы без корзины. Логика редиректа на оплату это уже не стили, поэтому такую кнопку я оборачиваю в отдельный компонент CheckoutButton, а не мешаю в общий Button.

Форма - это уже бизнес-логика

Форма заявки выглядит как обычный блок с полями, но внутри неё валидация, состояние отправки, обработка ошибок и вызов API. На части проектов форма шлёт заявку напрямую в CRM, на части в вебхук n8n, который уже сам раскладывает заявку по CRM, таблице и уведомлению в Telegram через aiogram-бота на бэкенде. С точки зрения компонента разницы нет, он просто дергает один URL и обрабатывает ответ. Такую логику я выношу в отдельный form-компонент и отдельный хук вида useLeadForm, который инкапсулирует запрос и состояние загрузки, а сам компонент формы занимается только полями и отображением ошибок. Такой подход экономит время и на бэкенде: фронтенд не знает и не должен знать, куда именно доходит заявка, он просто обращается к одному эндпоинту.

Если форма на лендинге начинает обрастать личным кабинетом, историей заявок или ролями пользователей, это уже другая задача по масштабу - в таких случаях предлагаю клиенту CRM или админ-панель на React вместо разрастания лендинга.

Когда компонент лендинга становится избыточным

Обратная сторона декомпозиции - переинжиниринг. Видел проекты, где Hero разбит на HeroTitle, HeroSubtitle, HeroButton и HeroWrapper, хотя все четыре используются один раз и нигде больше не переиспользуются. В итоге, чтобы понять, что выводится на экране, нужно прыгать по четырём файлам вместо одного, хотя вся секция целиком помещается в тридцать строк JSX. Похожая история с обёртками ради обёрток: SectionWrapper, который просто добавляет отступы вокруг Advantages и нигде больше не используется, проще держать обычным div с классом внутри самой секции.

Правило, которым пользуюсь сам: компонент выделяю, если выполняется хотя бы одно из двух условий - блок реально переиспользуется в двух и более местах, или у блока есть своя изолированная логика (состояние, запрос, валидация). Если ни того, ни другого нет, а разбивка идёт просто «потому что так аккуратнее выглядит», это лишний уровень вложенности, который потом придётся распутывать при рефакторинге.

То же самое с пропсами. Не стоит делать Advantages настраиваемым через десять пропсов, если он используется один раз на одном лендинге - проще прописать данные прямо внутри компонента или вынести в соседний файл-константу. Универсальность имеет смысл, только когда компонент реально ставится на второй и третий проект без переписывания половины кода.

Структура папок и переиспользование блоков между проектами

На лендингах на React и Next.js у меня устоялась одна и та же структура: UI-примитивы отдельно от секций страницы, секции отдельно от форм. Имена файлов у меня всегда совпадают с именем секции на макете - так дизайнер и разработчик говорят об одном и том же блоке без путаницы на созвонах.

components/
  ui/
    Button.tsx
    Container.tsx
    SectionTitle.tsx
  sections/
    Hero.tsx
    Advantages.tsx
    Pricing.tsx
    Testimonials.tsx
    Faq.tsx
    Footer.tsx
  forms/
    LeadForm.tsx
    useLeadForm.ts

Такое разделение даёт понятную точку входа: нужно поправить кнопку - иду в ui, нужно переставить блоки местами - иду в page.tsx, где секции просто импортируются по порядку. Между проектами чаще всего переезжают Header, Footer, Button и Container - это заготовки, которые почти не меняются от лендинга к лендингу. Hero, Advantages и Pricing переписываются заново под контент клиента. Такая структура также упрощает подключение сторонних инструментов вроде Storybook или визуальных снапшот-тестов - им достаточно точки входа в components/sections, а не всего приложения целиком.

Типизация пропсов между секциями

Каждой секции с данными я завожу интерфейс рядом с компонентом, а не общий тип на всю страницу. Для Pricing это обычно массив тарифов с полями вроде названия, цены и списка опций, для Testimonials - массив отзывов с именем, текстом и опциональной фотографией. Раздельные интерфейсы под каждую секцию избавляют от одного крупного типа на триста строк, где непонятно, какие поля вообще используются в конкретном блоке. Ошибка в лишнем поле сразу подсвечивается редактором ещё до сборки, а не превращается в баг на проде, который заметят только на демонстрации.

Производительность: ленивая загрузка нижних секций

Разбивка на компоненты сама по себе не ускоряет страницу, но она открывает дорогу к ленивой загрузке. Тут важно, как next/dynamic ведёт себя с серверными компонентами: при динамическом импорте серверного компонента отложенно загрузятся только клиентские компоненты внутри него, а сам серверный компонент отложенным не станет. Опция ssr: false в серверном компоненте не поддерживается и даёт ошибку. То есть выигрыш даёт не сама обёртка, а то, что тяжёлый клиентский код секции ниже первого экрана (слайдер отзывов, карта, календарь) уезжает в отдельный чанк:

import dynamic from 'next/dynamic'

const Testimonials = dynamic(() => import('@/components/sections/Testimonials'))

export default function LandingPage() {
  return (
    <main>
      <Hero />
      <Advantages />
      <Testimonials />
    </main>
  )
}

Если бы Testimonials тянул за собой слайдер весом в отдельный чанк, а весь код лежал в одном файле лендинга, разбить бандл было бы физически негде - пришлось бы грузить всё сразу. Отдельные компоненты по секциям - это ещё и отдельные точки разбивки бандла, а не только удобство чтения кода. То же самое с картинками внутри секций - если каждая секция сама решает, как оптимизировать свои изображения, легче найти место, где реально тормозит конкретный блок, а не гадать по всей странице целиком. На практике это особенно заметно на мобильном трафике, где лишние килобайты JS до первого взаимодействия ощущаются сильнее всего.

React регулярно обновляется, поэтому перед внедрением я сверяюсь с актуальной документацией.

Частые вопросы

Сколько React-компонентов должно быть на обычном лендинге?

Обычно 8-12 файлов: header, footer, 4-6 секций контента, форма, пара элементов UI-kit вроде кнопки и контейнера. Если счёт идёт на 30+ компонентов при одной странице, скорее всего декомпозиция зашла дальше, чем нужно, и часть файлов стоит объединить обратно.

Стоит ли использовать Server Components для лендинга на Next.js?

Для статичных секций без состояния да, они не тянут лишний JS на клиент и рендерятся на сервере. Форму с состоянием и обработчиками нужно помечать директивой use client, так что деление на компоненты заодно совпадает с границей между серверной и клиентской частью приложения.

Можно ли переиспользовать компоненты лендинга в интернет-магазине или другом проекте?

UI-примитивы вроде кнопок, контейнеров и типографики переезжают почти без изменений. Секции вроде Hero и Pricing обычно приходится переписывать под другую структуру данных, поэтому я не пытаюсь делать их универсальными заранее - это трата времени под гипотетическое будущее использование, которое может и не наступить.

Нужно ли выносить анимации в отдельные компоненты?

Если анимация - это обёртка вокруг чужого контента, например fade-in при скролле или появление карточки, отдельный обёрточный компонент удобнее, чем дублировать одну и ту же логику в каждой секции. Если анимация специфична для одного блока и больше нигде не встречается, оставляю её внутри самого блока, без лишнего слоя абстракции.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно