Привет, с вами снова Костя из Cloud.ru. Мы поговорили про паттерн as для типа безопасного полиморфизма и asChild для чистой композиции. Но сегодня поговорим о подходе, который дает такую гибкость в вариативном дизайне, что дизайнеры будут плакать от счастья — FACC (Function as Child Component).

Проблема: 15 вариантов карточки продукта

Представьте: у вас есть карточка товара, но дизайнеры нарисовали ее в 15 вариантах:

  • вертикальная с рейтингом,

  • горизонтальная со скидкой,

  • компактная для списков,

  • с видеопревью,

  • с быстрым добавлением в корзину,

  • ... и еще 10 вариаций.

Классический подход — пропсы на все случаи жизни:

<ProductCard
  orientation="vertical"
  showRating={true}
  showDiscount={false}
  compact={false}
  hasVideo={true}
  quickAdd={false}
  // ... ещё 10 пропсов
/>

Кошмар, но настоящая проблема даже не в количестве пропсов. Куда сложнее разобраться, что происходит внутри компонента, когда начинают плодиться подобные конструкции:

/* внутри ProductCard */
{videoUrl && showRating && compact && (
  <span>
    {/* какой-то особый текст если есть и видео, и рейтинг, и компактность */}
    Limited offer!
  </span>
)}

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

А если нужно комбинировать? «Вертикальная компактная с рейтингом и видео, но без цены» — это уже отдельный компонент или вас уже пора уволить за эти костыли?

FACC-решение: дизайн как API

А что, если дать разработчику полную свободу в компоновке, но оставить контролируемую логику?

<ProductCardFacc product={product}>
  {({ Title, Image, Price, Rating, AddToCart, Container }) => (
    <Container className="custom-layout">
      <Image size="large" />
      <Title variant="h2" maxLines={2} />
      <Rating showCount={true} />
      <Price fontSize="xl" />
      <AddToCart variant="primary" />
    </Container>
  )}
</ProductCardFacc>

Обратите внимание, в этом коде разработчик компонента Card не стал указывать, как расположить заголовок или рейтинг, а просто выдал набор готовых блоков, оставив верстку этих элементов на наше усмотрение. Немного знаний математики нужно, чтобы сказать, что здесь как минимум 120 (5!) вариаций карточки, не считая неограниченного набора кастомных компонентов, которые ничем не запрещены!

Не все так однозначно

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

Как это работает? Контролируемая гибкость

function ProductCardFacc({ product, children }) {
  // Вся бизнес-логика здесь
  const { title, image, price, rating, inStock } = product;

  // Здесь мы объявим сами компоненты и опишем их по дизайн-системе
  const components = {
    Title: ({ variant = "h3", maxLines, ...props }) => (
      <Typography variant={variant} maxLines={maxLines} {...props}>
        // Внутри будем использовать правильные данные
        {title}
      </Typography>
    ),
    
    Image: ({ size = "medium", ...props }) => (
      <Image 
        src={image} 
        size={size}
        alt={title}
        {...props}
      />
    ),
    
    // Все остальные компоненты тоже тут
  };

  return children(components);
}

Этот код прекрасен по двум причинам. Во-первых, мы полноценно отделяем логику от отрисовки, а нет ничего прекраснее для инженера, чем разделить модели и вью. Во-вторых, мы полноценно получаем гибкий и неограниченно расширяемый компонент, который забирает на себя логику дизайн-системы. Если нам понадобится карточка, в которой есть рейтинг, кнопка, изображение (что мы уже добавили) и, например, тег sale в правом верхнем углу, нам ничего не будет стоит дописать его в том месте, где мы строим карточку, не задевая при этом остальные элементы.

Глянем на живой код

Вертикальная карточка для каталога:

<ProductCardFacc product={product}>
  {({ Image, Title, Price, AddToCart }) => (
    <div className="vertical-card">
      <Image size="large" />
      <Title variant="h3" maxLines={2} />
      <Price fontSize="lg" showOriginal={true} />
      <AddToCart variant="primary" fullWidth />
    </div>
  )}
</ProductCardFacc>

Горизонтальная для корзины:

<ProductCardFacc product={product}>
  {({ Image, Title, Price, Rating }) => (
    <div className="horizontal-card">
      <Image size="small" />
      <div className="content">
        <Title variant="body" maxLines={1} />
        <Rating showCount={true} />
        <Price fontSize="md" />
      </div>
    </div>
  )}
</ProductCardFacc>

Компактная для списка сравнения:

<ProductCardFacc product={product}>
  {({ Image, Title, Price }) => (
    <div className="compact-card">
      <Image size="tiny" />
      <Title variant="small" maxLines={1} />
      <Price fontSize="sm" />
    </div>
  )}
</ProductCardFacc>

Как видим, такой подход позволяет генерировать сколько угодно разных видов карточек, не теряя в дизайне и имея большую гибкость в разработке. Главное — не увлекаться вложенными FACC, иначе читаемость JSX может снизиться, а проект обрастет разрозненными вариантами.

Преимущества перед другими паттернами

Не так просто сравнивать его с классическими паттернами полиморфизма, так как FACC решает немного другую проблему: он нацелен на создание комплексных вариативных компонентов. Тем не менее, мы попробуем создать тот же код при помощи других паттернов и посмотрим, куда нас это приведет.

Против as:

// as-подход требует клонирования всех вариантов и не дает нужной гибкости
// Каждый вариант — отдельный компонент, который нельзя построить из "кирпичей"
<ProductCard as={VerticalCard} />
<ProductCard as={HorizontalCard} /> 
<ProductCard as={CompactCard} />

Против asChild:

// asChild даёт гибкость, но без контролируемого API
<ProductCard asChild>
  <div>
    {/* Можно сделать что угодно, даже сломать логику */}
    <WrongComponent />
    <AnotherWrongComponent />
  </div>
</ProductCard>

Против компонентов типа Card.Title:

// Классический подход с вложенными компонентами
<ProductCard>
  <ProductCard.Image />
  <ProductCard.Body>
    <ProductCard.Title />
    <ProductCard.Price />
    <ProductCard.Rating />
  </ProductCard.Body>
  <ProductCard.Actions>
    <ProductCard.AddToCart />
  </ProductCard.Actions>
</ProductCard>

Последний пример самый интересный, потому что он, можно сказать, родственник FACC. Вместе с базовым компонентом мы импортируем из источника его части, а потом можем использовать внутри по тому же принципу.

Но как мы получаем данные внутри самих компонентов? В этом подходе мы с большой вероятностью используем хуки и кастомные контексты, однако узнать о том, что какой-то ProductCard.Title был использован внутри вложенных компонентов и оказался использован там, где контекста нет (в другом месте), мы можем только запустив проект.

Именно это и есть важная проблема по сравнению со старичком FACC — невозможность нормально типизировать и версионировать те компоненты, которые вы используете в качестве отдельных блоков.

Типизация — легко!

type ProductComponents = {
  Title: ComponentType<{ variant?: string; maxLines?: number }>;
  Image: ComponentType<{ size?: 'small' | 'medium' | 'large' }>;
  Price: ComponentType<{ showOriginal?: boolean; fontSize?: string }>;
  Rating: ComponentType<{ showCount?: boolean }>;
  AddToCart: ComponentType<{ variant?: string; fullWidth?: boolean }>;
}

type ProductCardFaccProps = {
  product: Product;
  children: (components: ProductComponents) => ReactElement;
}

Расширяемость — легко!

Когда дизайнеры придумали «бейдж акции», мы просто добавляем:

const components = {
  /* ... существующие компоненты */
  DiscountBadge: ({ position = "top-right", ...props }) => (
    product.discount > 0 && (
      <Badge position={position} {...props}>
        -{product.discount}%
      </Badge>
    )
  )
};

И любой компонент может добавить себе этот бейдж, просто получив его внутри компонента через intelisens.

Где еще сияет FACC

Формы с вариативным UI:

<FormFacc form={form}>
  {({ Field, Submit, ErrorMessage }) => (
    <>
      <Field name="email" type="email" />
      <Field name="password" type="password" />
      <ErrorMessage name="general" />
      <Submit label="Войти" />
    </>
  )}
</FormFacc>

Форма, которая берет на себя всю логику, оставляя на разработчика только лишь скомпоновать элементы — «Клондайк» разработки для больших и сложных админок с кучей таких форм.

Data-компоненты (частный случай Card):

<UserProfileFacc userId="123">
  {({ Avatar, Name, Bio, Stats, FollowButton }) => (
    <div className="profile">
      <Avatar size="large" />
      <Name variant="h1" />
      <Bio maxLines={3} />
      <Stats layout="horizontal" />
      <FollowButton variant="outline" />
    </div>
  )}
</UserProfileFacc>

Здесь мы говорим о том, что любые данные можно представлять по-разному, поэтому выгодно для владельца бизнес-логики выдать набор готовых элементов и взять на себя обработку этих самых данных. Как их разложить — решение за вами!

Выводы

FACC - это не просто еще один паттерн, это философия компонентного дизайна.

  • Контролируемая гибкость — можно комбинировать, но нельзя сломать.

  • Естественная композиция — JSX остается читаемым.

  • Легкая расширяемость — новые элементы добавляются без боли.

  • Переиспользуемая логика — бизнес-правила в одном месте.

В нашей дизайн-системе FACC отлично решает задачу создания комплексных и очень вариативных компонентов, например карточек, профилей и форм. Он становится настоящим спасением там, где другие паттерны бессильны. Главное — помнить, что чрезмерное усложнение JSX или злоупотребление композицией (например, глубокие вложенные FACC) могут ухудшить поддерживаемость кода и запутать команду. В остальном это мощный инструмент для архитекторов UI.

А вы используете FACC для вариативного дизайна? Или предпочитаете другие подходы? Делитесь кейсами в комментах!

P.S. В следующей статье разберем, как окончательно уйти от полиморфизма, но использовать его код для создания настоящих монстров чистого кода — полиморфных декораторов.

Комментарии (1)


  1. Alexandroppolus
    25.11.2025 12:43

    У подхода есть и свои минусы. Например, если взять текущую реализацию ProductCardFacc, то при любом ререндере этого компонента создаются новые Image, Title и т.д., то есть все они будут перемонтированы, что мягко говоря не фонтан. Можно уменьшить проблему, если их создавать через useCallback, но даже отдельные перемонтирования при изменении пропса - тоже так себе, например, прощай анимация. Более классический React-way - в компоненте ProductCardFacc просто сложить значения в контексты (или в один контекст, если у вас например mobx), но тогда компоненты можно просто заимпортировать, а в children передавать не функцию, а сразу верстку, и паттерн разваливается на глазах..