Controlled components

Controlled components представляют собой компоненты, состояние которых полностью управляется извне через свойства (props). В отличие от uncontrolled компонентов, где внутреннее состояние управляется самим компонентом, controlled components полагаются на данные, переданные извне, и события, уведомляющие об изменениях. Такой подход обеспечивает единую точку управления состоянием и упрощает синхронизацию данных между компонентами.


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

  1. Состояние управляется через свойства Компонент принимает значение через @Prop() и не хранит его копию в @State(). Любое изменение должно инициироваться внешним кодом.

    import { Component, Prop, h, Event, EventEmitter } from '@stencil/core';
    
    @Component({
      tag: 'controlled-input',
      styleUrl: 'controlled-input.css',
      shadow: true,
    })
    export class ControlledInput {
      @Prop() value: string = '';
      @Event() valueChange: EventEmitter<string>;
    
      handleInput(event: Event) {
        const input = event.target as HTMLInputElement;
        this.valueChange.emit(input.value);
      }
    
      render() {
        return <input value={this.value} onIn put={e => this.handleInput(e)} />;
      }
    }

    В примере выше значение инпута полностью управляется извне. Компонент лишь уведомляет о каждом изменении через событие valueChange.

  2. События как основной механизм синхронизации Controlled components должны испускать события при каждом изменении состояния. Это позволяет родительскому компоненту перехватывать изменения и обновлять пропсы.

    <controlled-input value={this.parentValue} onValueCha nge={e => this.parentValue = e.detail}></controlled-input>

    Здесь parentValue является источником правды, а компонент controlled-input просто отражает это значение.

  3. Отсутствие локального состояния Любое хранение состояния в @State() для контролируемых компонентов противоречит принципу «единое место правды». Локальный @State() может использоваться только для вспомогательных данных, не влияющих на внешний API компонента.


Взаимодействие с формами

Controlled components идеально подходят для работы с формами, где требуется единственное централизованное состояние:

@Component({
  tag: 'form-example',
  shadow: true
})
export class FormExample {
  @State() username: string = '';

  handleUsernameChange(value: string) {
    this.username = value;
  }

  render() {
    return (
      <form>
        <controlled-input value={this.username} onValueCha nge={e => this.handleUsernameChange(e.detail)} />
        <button type="submit">Submit</button>
      </form>
    );
  }
}

В этом примере ввод пользователя немедленно синхронизируется с состоянием родителя, обеспечивая предсказуемость и возможность валидации на лету.


Преимущества controlled components

  • Предсказуемость: состояние компонента полностью контролируется родителем, исключая расхождения.
  • Упрощение тестирования: можно легко подставлять любые значения пропсов и проверять реакцию компонента.
  • Централизованная логика: валидация, форматирование и другие преобразования выполняются на уровне родителя.
  • Совместимость с глобальными состояниями: легко интегрируются с Redux, Zustand или Context API.

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

  1. Использование @Event() и EventEmitter Для передачи изменений компонентам-родителям обязательно использовать события. Прямое изменение @Prop() невозможно, так как они только для чтения.

  2. Shadow DOM и рефы Если компонент использует Shadow DOM, необходимо корректно проксировать события. В Stencil события, испущенные через EventEmitter, автоматически всплывают, что упрощает интеграцию.

  3. Слияние с uncontrolled логикой Иногда нужно реализовать гибридный компонент, который может быть как controlled, так и uncontrolled. Для этого используют проверку на наличие пропа:

    @Prop() value?: string;
    @State() internalValue: string = '';
    
    get currentValue() {
      return this.value !== undefined ? this.value : this.internalValue;
    }
    
    handleInput(event: Event) {
      const input = event.target as HTMLInputElement;
      if (this.value === undefined) {
        this.internalValue = input.value;
      }
      this.valueChange.emit(input.value);
    }

    Такой подход позволяет компоненту работать автономно, если родитель не передал value.


Практические рекомендации

  • Всегда определять событие с названием по шаблону <propName>Change для единообразия.
  • Не хранить дублирующееся состояние внутри компонента без необходимости.
  • Обрабатывать все изменения только через события, избегая прямых манипуляций с DOM.
  • Документировать API компонента, чтобы внешние разработчики понимали, что компонент controlled.

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