Подъем состояния

Haunted — это лёгкая библиотека для создания веб-компонентов на базе хуков, вдохновлённая React, но использующая стандартные возможности Web Components. Основной задачей библиотеки является управление состоянием компонента и его реактивное обновление. Рассмотрим подробно механизмы подъёма состояния и передачи данных между компонентами.


Принцип работы состояния

Состояние в Haunted создаётся с помощью хука useState. Этот хук возвращает пару значений: текущее состояние и функцию для его обновления.

import { html, component, useState } from 'haunted';

function Counter() {
  const [count, setCount] = useState(0);

  return html`
    <div>
      <p>Счётчик: ${count}</p>
      <button @click=${() => setCount(count + 1)}>Увеличить</button>
    </div>
  `;
}

customElements.define('my-counter', component(Counter));

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

  • useState(initialValue) создаёт локальное состояние компонента.
  • setCount(newValue) обновляет состояние и автоматически перерисовывает компонент.
  • Любое изменение состояния вызывает реактивное обновление шаблона.

Почему возникает потребность в подъёме состояния

Локальное состояние ограничено компонентом, в котором оно создано. Когда несколько компонентов должны разделять данные или реагировать на одни и те же события, локального состояния недостаточно. Например, родительский компонент должен знать значение счётчика, находящегося в дочернем компоненте. Решение — подъём состояния (lifting state up).


Подъём состояния: базовый пример

Подъём состояния заключается в перемещении состояния из дочернего компонента в родительский и передаче функции обновления вниз через свойства.

function Parent() {
  const [count, setCount] = useState(0);

  return html`
    <div>
      <p>Общий счётчик: ${count}</p>
      <my-counter .count=${count} .setCount=${setCount}></my-counter>
    </div>
  `;
}

function Child({ count, setCount }) {
  return html`
    <div>
      <p>Счётчик в дочернем: ${count}</p>
      <button @click=${() => setCount(count + 1)}>Увеличить</button>
    </div>
  `;
}

customElements.define('my-counter', component(Child));
customElements.define('my-parent', component(Parent));

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

  • Родитель создаёт состояние и функцию обновления (count и setCount).
  • Дочерний компонент получает их через свойства (props).
  • Любое изменение состояния в дочернем компоненте отражается в родителе, обеспечивая синхронизацию данных.

Работа с несколькими состояниями

Haunted позволяет использовать несколько хуков состояния в одном компоненте:

function Form() {
  const [name, setName] = useState('');
  const [age, setAge] = useState('');

  return html`
    <input 
      placeholder="Имя" 
      .value=${name} 
      @input=${e => setName(e.target.value)}
    />
    <input 
      type="number" 
      placeholder="Возраст" 
      .value=${age} 
      @input=${e => setAge(e.target.value)}
    />
    <p>Имя: ${name}, Возраст: ${age}</p>
  `;
}

customElements.define('my-form', component(Form));

Выводы:

  • Каждый вызов useState создаёт отдельную реактивную переменную.
  • Подъём нескольких состояний осуществляется аналогично: их объединяют в родительском компоненте и передают вниз через props.

Контроль подъёма состояния с useReducer

Для сложных состояний, когда необходимо управлять множественными связанными данными, предпочтительнее использовать useReducer вместо множества useState. Он работает по принципу редьюсера Redux:

import { useReducer } from 'haunted';

function reducer(state, action) {
  switch(action.type) {
    case 'increment': return { ...state, count: state.count + 1 };
    case 'decrement': return { ...state, count: state.count - 1 };
    default: return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return html`
    <p>Счётчик: ${state.count}</p>
    <button @click=${() => dispatch({ type: 'increment' })}>+</button>
    <button @click=${() => dispatch({ type: 'decrement' })}>-</button>
  `;
}

customElements.define('my-counter', component(Counter));

Преимущества useReducer:

  • Логика изменения состояния централизована в функции reducer.
  • Удобно поднимать сложное состояние в родителя без передачи множества отдельных функций.

Передача событий для подъёма состояния

Иногда вместо прямой передачи функции обновления используется кастомное событие, которое дочерний компонент испускает при изменении состояния. Родитель ловит событие и обновляет своё состояние:

function Child() {
  const [value, setValue] = useState('');

  const handleInput = e => {
    setValue(e.target.value);
    e.target.dispatchEvent(new CustomEvent('value-changed', {
      detail: e.target.value,
      bubbles: true
    }));
  };

  return html`<input .value=${value} @input=${handleInput} />`;
}

function Parent() {
  const [text, setText] = useState('');

  const handleChange = e => setText(e.detail);

  return html`
    <p>Текст: ${text}</p>
    <my-child @value-changed=${handleChange}></my-child>
  `;
}

customElements.define('my-child', component(Child));
customElements.define('my-parent', component(Parent));

Особенности:

  • Использование событий удобно для слабосвязанных компонентов.
  • Позволяет поднимать состояние без прямой передачи функций.
  • События могут быть всплывающими, что упрощает обработку на разных уровнях иерархии.

Итоговые рекомендации

  • Локальное состояние следует использовать только для внутреннего состояния компонента.
  • Подъём состояния необходим, когда несколько компонентов должны разделять одни и те же данные.
  • Для простых случаев достаточно передачи функции обновления через свойства.
  • Для сложных объектов состояния предпочтительнее useReducer или кастомные события.
  • Всегда учитывать реактивность Haunted: любое обновление состояния перерисовывает шаблон компонента.

Haunted обеспечивает мощный, но лёгкий механизм управления состоянием, который в сочетании с подъёмом состояния позволяет строить сложные и реактивные интерфейсы, сохраняя модульность и чистоту кода.