Сложная логика состояния

Haunted — это библиотека для создания веб-компонентов на основе Web Components с использованием реактивного подхода, схожего с React Hooks. Центральное место в Haunted занимает управление состоянием компонентов и реактивное обновление интерфейса.

Хук useState

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>
      <button @click=${() => setCount(count - 1)}>Уменьшить</button>
    </div>
  `;
}

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

Особенности использования:

  • При вызове setCount компонент перерисовывается.
  • Можно хранить сложные объекты, массивы или вложенные структуры.
  • Для объектов и массивов важно создавать новый экземпляр при обновлении, чтобы сработала реактивность:
setItems([...items, newItem]);

Хук useEffect

useEffect позволяет реагировать на изменения состояния или жизненный цикл компонента.

import { useEffect } from 'haunted';

useEffect(() => {
  console.log('Компонент смонтирован');
  return () => console.log('Компонент размонтирован');
}, []);
  • Второй аргумент — массив зависимостей. Если пустой, эффект выполняется только при монтировании и размонтировании.
  • Можно реагировать на конкретные состояния:
useEffect(() => {
  console.log(`Счётчик изменился: ${count}`);
}, [count]);

Хук useReducer

Для сложной логики состояния лучше использовать useReducer. Он позволяет управлять состоянием через редьюсер — функцию, которая принимает текущее состояние и действие, возвращая новое состояние.

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

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

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

Композиция хуков

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

function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  return { count, increment, decrement };
}

function CounterComponent() {
  const counter = useCounter(5);

  return html`
    <div>
      <p>Счётчик: ${counter.count}</p>
      <button @click=${counter.increment}>+</button>
      <button @click=${counter.decrement}>-</button>
    </div>
  `;
}

Такой подход повышает читаемость и позволяет переиспользовать логику между компонентами.

Управление массивами и объектами

Для массивов и объектов важно помнить о неизменяемости состояния.

function TodoList() {
  const [todos, setTodos] = useState([]);

  const addTodo = todo => setTodos([...todos, todo]);
  const removeTodo = index => setTodos(todos.filter((_, i) => i !== index));

  return html`
    <ul>
      ${todos.map((todo, i) => html`<li>${todo} <button @click=${() => removeTodo(i)}>Удалить</button></li>`)}
    </ul>
    <button @click=${() => addTodo('Новое дело')}>Добавить</button>
  `;
}
  • Любое изменение массива или объекта должно создавать новый экземпляр, чтобы компонент перерисовался.
  • Прямое изменение элементов (todos.push(...)) не вызовет обновление интерфейса.

Асинхронное состояние

Haunted позволяет работать с асинхронными операциями. Чаще всего используется useEffect с асинхронными функциями:

useEffect(() => {
  let canceled = false;

  async function fetchData() {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
    const data = await response.json();
    if (!canceled) setData(data);
  }

  fetchData();

  return () => { canceled = true };
}, []);
  • Переменная canceled предотвращает обновление состояния после размонтирования компонента.
  • Асинхронные операции не блокируют рендеринг.

Паттерны сложной логики

  1. Разделение состояния на хуки: каждый аспект состояния выносится в отдельный хук (useCounter, useTodos).
  2. Централизованные редьюсеры: объединение нескольких редьюсеров для сложных форм или больших компонентов.
  3. Использование эффектов для синхронизации: связь состояния с локальным хранилищем, сервером или другими компонентами.

Взаимодействие с атрибутами компонента

Состояние можно синхронизировать с атрибутами веб-компонента через хуки useState или useReducer, обрабатывая их в attributeChangedCallback.

function MyComponent({ initial }) {
  const [value, setValue] = useState(initial);

  return html`<p>Значение: ${value}</p>`;
}
  • Изменения атрибута могут триггерить обновление состояния.
  • Используется для передачи внешних данных внутрь компонента при его инициализации.

Рекомендации по организации состояния

  • Хранить только минимально необходимое состояние.
  • Выносить сложную логику в отдельные хуки.
  • Всегда создавать новые объекты при обновлении массивов и объектов.
  • Использовать useEffect для побочных эффектов, асинхронных операций и синхронизации.

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