Композиция и темизация компонентов — Примеры

Пример 1 — Тема, которая переопределяет компонент снаружи

Тёмная тема побеждает стили кнопки более длинным селектором, и селектор уже вырос однажды: скажите, чего стоит переопределение, и дайте теме поставлять значения вместо этого.

.button { background: navy; color: white; padding: 0.5rem 1rem; border-radius: 0.25rem; }

.theme-dark .panel .button {           /* побеждает компонент специфичностью */
  background: #111;
  padding: 0.5rem 1rem;
  border-radius: 0.25rem;
}

Решение

.button {
  --control-pad: 0.5rem 1rem;
  --control-radius: 0.25rem;
  background: var(--button-bg, navy);
  color: var(--button-fg, white);
  padding: var(--control-pad);
  border-radius: var(--control-radius);
}

.theme-dark .panel { --button-bg: #111; --button-fg: #eee; }
<div className="theme-dark">
  <div className="panel"><Button variant="primary">Сохранить</Button></div>
</div>

Переопределение побеждает специфичностью, поэтому каждой новой теме нужен селектор длиннее предыдущего, и компонент нельзя отрефакторить, не сломав их. Пользовательские свойства переворачивают направление: компонент читает значение, а тема задаёт его на обёртке, и поскольку свойства наследуются, одна обёртка темизирует всё внутри без селектора на каждый компонент. Запасное значение внутри var() защищает компонент, когда его никто не задал. Всё, что потребитель может задать, — публичный API, поэтому имена свойств документируют и переименовывают только как версионированное изменение.

Пример 2 — Компонент, который скопировали ради одного экрана

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

function Card({ title, body }) {
  return (
    <article className="card">
      <h2>{title}</h2>
      <p>{body}</p>
    </article>
  );
}

// в другом месте: CardWithActions, затем CardWithActionsAndBadge, обе копии

Решение

function Card({ title, body, actions }) {
  return (
    <article className="card">
      <h2>{title}</h2>
      <p>{body}</p>
      {actions ? <div className="card-actions">{actions}</div> : null}
    </article>
  );
}
<Card
  title="Счёт №1042"
  body={summary}
  actions={<><Button variant="primary">Оплатить</Button><Button>Скачать</Button></>}
/>

Содержимое, переданное как дочерние элементы или слот, можно компоновать так, как автор компонента не предполагал, а копию можно расширить только скопировав её снова, — именно так две копии стали тремя. Вариация была недостающим слотом, а не новым компонентом, и добавить слот дешевле, чем поддерживать форки, которые разойдутся снова при следующем изменении дизайна. Слот ещё и не пускает вызывающего во внутренности компонента: внутреннее имя класса не передаётся, поэтому разметку можно перестроить позже, не трогая ни одного места вызова.