Паттерн provider/consumer

Slim.js — легковесная библиотека для создания веб-компонентов на JavaScript с реактивной системой данных. Одним из ключевых паттернов для построения масштабируемых компонентов является provider/consumer, который позволяет организовать передачу данных и зависимостей между компонентами без необходимости пробрасывать их через каждый уровень дерева компонентов.


Основные концепции

Provider — это компонент, который предоставляет данные или сервисы. Он создаёт контекст, доступный для всех его потомков, которые захотят использовать эти данные.

Consumer — компонент, который получает данные от ближайшего родителя-provider. Consumer подписывается на изменения данных и автоматически обновляется при их изменении.

Ключевые преимущества паттерна:

  • Упрощение управления состоянием на разных уровнях дерева компонентов.
  • Минимизация необходимости проброса props через множество вложенных компонентов.
  • Чёткая инкапсуляция зависимостей.

Создание Provider

Для определения provider используется свойство provide в компоненте. Оно может принимать объект с любыми значениями, которые должны быть доступны потомкам.

Пример:

import { Slim } from 'slim-js';

class ThemeProvider extends Slim {
  static get observedAttributes() {
    return ['theme'];
  }

  constructor() {
    super();
    this.theme = 'light';
  }

  provide() {
    return {
      theme: this.theme
    };
  }

  onAttributeChange(attr, oldVal, newVal) {
    if (attr === 'theme') {
      this.theme = newVal;
      this.update(); // обновление компонента и передача нового значения потомкам
    }
  }

  render() {
    return `
      <slot></slot>
    `;
  }
}

customElements.define('theme-provider', ThemeProvider);

Ключевые моменты:

  • Метод provide() возвращает объект, доступный для всех потомков.
  • Любое изменение состояния, влияющее на данные provider, должно инициировать обновление (update()), чтобы потребители получили актуальные значения.

Создание Consumer

Consumer использует свойство consume для подписки на данные provider. Slim.js автоматически ищет ближайший ancestor с соответствующим provide и связывает данные.

Пример:

class ThemeConsumer extends Slim {
  consume() {
    return ['theme'];
  }

  render() {
    return `
      <div>Текущая тема: ${this.theme}</div>
    `;
  }
}

customElements.define('theme-consumer', ThemeConsumer);

Особенности работы:

  • consume() возвращает массив имён свойств, которые компонент хочет получать от provider.
  • При изменении этих свойств consumer автоматически перерисовывается, обеспечивая реактивность.

Вложенные Provider

Slim.js поддерживает иерархию provider/consumer. В этом случае каждый consumer получает данные от ближайшего provider в дереве компонентов.

Пример вложенности:

<theme-provider theme="dark">
  <div>
    <theme-consumer></theme-consumer>
    <theme-provider theme="light">
      <theme-consumer></theme-consumer>
    </theme-provider>
  </div>
</theme-provider>
  • Первый consumer получает theme: dark.
  • Второй consumer получает theme: light, так как внутренний provider перекрывает внешний.

Динамическое обновление данных

Provider может обновлять данные в любой момент, и все подписанные consumer автоматически получат новые значения.

setTimeout(() => {
  document.querySelector('theme-provider').theme = 'dark';
}, 2000);

После обновления theme все consumer перерисуются с новым значением.


Практическое использование

  1. Темизация интерфейса — передача темы, стилей, цветовой схемы.
  2. Глобальные состояния — пользователь, настройки приложения, локализация.
  3. Сервисы — API-клиенты, менеджеры уведомлений, кеши данных.

Ограничения и рекомендации

  • Provider должен минимизировать лишние обновления, чтобы не перегружать потребителей.
  • Consumer не должен изменять данные provider напрямую; изменения должны идти через события или методы provider.
  • Для сложных деревьев компонентов рекомендуется разделять разные виды данных на отдельные provider, чтобы снизить зависимость и повысить повторное использование.

Взаимодействие с событиями

Хотя provider/consumer решает большинство задач передачи данных, иногда нужны пользовательские события. Slim.js позволяет комбинировать оба подхода:

class CounterProvider extends Slim {
  constructor() {
    super();
    this.count = 0;
  }

  provide() {
    return { count: this.count };
  }

  increment() {
    this.count++;
    this.update();
    this.dispatchEvent(new CustomEvent('count-changed', { detail: this.count }));
  }

  render() {
    return `<slot></slot>`;
  }
}

Consumer может подписываться на событие count-changed для дополнительных действий, помимо получения данных через consume().


Паттерн provider/consumer в Slim.js обеспечивает мощный и реактивный способ организации данных между компонентами, позволяя строить гибкие, поддерживаемые и масштабируемые приложения без сложного проброса props через множество уровней.