Provider паттерн

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

Provider паттерн в Stencil используется для управления состоянием приложения и передачи данных между компонентами без необходимости прокидывать свойства через каждый уровень иерархии. Этот подход особенно актуален в больших приложениях, где компоненты могут находиться на разных уровнях DOM, а прямое использование props или событий становится неудобным.

В основе паттерна лежит два ключевых элемента: Provider и Consumer. Provider создаёт контекст данных и обеспечивает доступ к этим данным для всех дочерних компонентов, которые подписаны на этот контекст через Consumer.

Создание контекста

Stencil предоставляет встроенный механизм контекста через функции createContext и provideContext. Контекст — это объект, содержащий состояние и методы для его изменения.

import { createContext } from '@stencil/core';

export interface AppContextState {
  user: string;
  isAuthenticated: boolean;
  login: (user: string) => void;
  logout: () => void;
}

export const AppContext = createContext<AppContextState>();

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

Реализация Provider компонента

Provider компонент создаётся с использованием декоратора @Component и метода provideContext. Он инкапсулирует логику управления состоянием и передаёт данные через контекст:

import { Component, h, State, Provide } from '@stencil/core';
import { AppContext, AppContextState } from './app-context';

@Component({
  tag: 'app-provider',
  shadow: true,
})
export class AppProvider {
  @State() user: string = '';
  @State() isAuthenticated: boolean = false;

  login = (user: string) => {
    this.user = user;
    this.isAuthenticated = true;
  };

  logout = () => {
    this.user = '';
    this.isAuthenticated = false;
  };

  @Provide() appContext: AppContextState = {
    user: this.user,
    isAuthenticated: this.isAuthenticated,
    login: this.login,
    logout: this.logout,
  };

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

Особенности реализации:

  • @Provide() связывает данные компонента с контекстом, который будет доступен всем дочерним компонентам.
  • Использование @State() обеспечивает реактивность: изменения состояния автоматически обновляют все подписанные на контекст компоненты.
  • render возвращает <slot />, чтобы все дочерние элементы могли быть вложены в Provider.

Подписка на контекст через Consumer

Дочерние компоненты получают доступ к данным контекста с помощью декоратора @Consume:

import { Component, h, Consume } from '@stencil/core';
import { AppContext, AppContextState } from './app-context';

@Component({
  tag: 'user-status',
  shadow: true,
})
export class UserStatus {
  @Consume(AppContext) appContext: AppContextState;

  render() {
    return (
      <div>
        {this.appContext.isAuthenticated
          ? `Пользователь: ${this.appContext.user}`
          : 'Гость'}
        <button onCl ick={() => this.appContext.logout()}>Выйти</button>
      </div>
    );
  }
}

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

  • @Consume(AppContext) автоматически подписывает компонент на обновления контекста.
  • Любые изменения состояния в Provider моментально отражаются в Consumer компонентах.
  • Consumer не должен знать, откуда пришли данные, что обеспечивает слабую связанность компонентов.

Преимущества использования Provider паттерна

  1. Централизованное управление состоянием: все данные и функции управления находятся в одном месте.
  2. Снижение связности компонентов: дочерние элементы не зависят от структуры дерева и могут быть использованы повторно.
  3. Реактивность: изменение состояния в Provider автоматически обновляет все подписанные компоненты.
  4. Типизация и безопасность: TypeScript гарантирует соответствие структуры данных интерфейсу контекста.

Рекомендации по применению

  • Использовать Provider паттерн для глобальных данных, таких как авторизация, настройки приложения, темы.
  • Для локальных данных, доступных только одному компоненту и его непосредственным потомкам, предпочтительно использовать @State и props.
  • Избегать чрезмерного количества уровней вложенности Provider, чтобы не усложнять дерево компонентов.

Особенности в Stencil

  • В отличие от React, Stencil контекст сохраняет реактивность только при использовании @State или @Prop внутри Provider. Простое присваивание объекта контекста без реактивных полей не вызовет обновление Consumer.
  • Контекст можно комбинировать с событиями (@Event) для передачи сигналов, которые не влияют на состояние, но требуют уведомления компонентов.
  • Stencil поддерживает shadow DOM, поэтому контекст работает даже при строгой изоляции стилей и структуры DOM.

Расширенные сценарии

  1. Множественные Provider: можно создавать несколько контекстов для разных областей приложения, например, AuthContext, ThemeContext, CartContext.
  2. Асинхронное обновление состояния: методы контекста могут быть асинхронными, что позволяет использовать API-запросы или загрузку данных из IndexedDB.
  3. Комбинирование с хуками: Stencil поддерживает использование хуков внутри функций компонентов, что позволяет подписываться на контекст без классовых компонентов.

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