Персистентность состояния

Библиотека Haunted строится на концепциях веб-компонентов и реактивного состояния, вдохновлённого React-хуками. Важным аспектом разработки компонентов является управление состоянием, которое должно сохраняться между перерисовками и, при необходимости, между сессиями. Персистентность состояния позволяет сохранять данные компонента в локальном хранилище, сеансовом хранилище или других источниках, обеспечивая непрерывность пользовательского опыта.


Управление локальным состоянием

Основной инструмент для работы с состоянием в Haunted — хук useState. Он обеспечивает реактивность и триггерит обновление DOM при изменении значения:

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));

Здесь count существует только во время жизни компонента. При перезагрузке страницы значение сбрасывается. Для персистентности нужно хранить состояние вне компонента.


Хранение состояния в localStorage

localStorage предоставляет возможность сохранять данные между сессиями браузера. Для интеграции с Haunted используется эффект useEffect и чтение/запись данных при инициализации и изменении состояния:

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

function PersistentCounter() {
  const [count, setCount] = useState(() => {
    const saved = localStorage.getItem('count');
    return saved ? Number(saved) : 0;
  });

  useEffect(() => {
    localStorage.setItem('count', count);
  }, [count]);

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

customElements.define('persistent-counter', component(PersistentCounter));

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

  • Использование ленивой инициализации useState(() => ...) позволяет прочитать значение из localStorage только один раз при создании компонента.
  • Эффект useEffect с зависимостью [count] обеспечивает синхронизацию состояния с хранилищем после каждого изменения.
  • Данные сохраняются даже после перезагрузки страницы.

Персистентность через sessionStorage

Если требуется сохранять состояние только на время текущей сессии, применяется sessionStorage. Подход идентичен localStorage, меняется только API:

function SessionCounter() {
  const [count, setCount] = useState(() => {
    const saved = sessionStorage.getItem('count');
    return saved ? Number(saved) : 0;
  });

  useEffect(() => {
    sessionStorage.setItem('count', count);
  }, [count]);

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

sessionStorage очищается при закрытии вкладки, поэтому подходит для временных данных.


Композиция хуков для сложных структур

Для объектов и массивов персистентность требует сериализации:

function TodoList() {
  const [todos, setTodos] = useState(() => {
    const saved = localStorage.getItem('todos');
    return saved ? JSON.parse(saved) : [];
  });

  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  const addTodo = task => setTodos([...todos, { task, done: false }]);
  const toggleTodo = index => setTodos(
    todos.map((t, i) => i === index ? { ...t, done: !t.done } : t)
  );

  return html`
    <div>
      <ul>
        ${todos.map((t, i) => html`
          <li>
            <input type="checkbox" .checked=${t.done} @change=${() => toggleTodo(i)}>
            ${t.task}
          </li>
        `)}
      </ul>
      <button @click=${() => addTodo('Новая задача')}>Добавить</button>
    </div>
  `;
}

Выделение ключевых моментов:

  • JSON-сериализация необходима для объектов и массивов.
  • Любое изменение структуры данных должно вызывать обновление localStorage через эффект.
  • Реактивность Haunted сохраняется при работе с комплексными структурами, если использовать иммутабельные операции (map, spread operator).

Обработка ошибок и синхронизация

При работе с персистентным хранилищем важно учитывать возможные ошибки:

  • Некорректный JSON при чтении (JSON.parse) может вызвать исключение.
  • Ограничения размера хранилища (обычно ~5 МБ).
  • Одновременное изменение состояния в нескольких вкладках требует прослушивания события storage:
useEffect(() => {
  const handler = e => {
    if (e.key === 'count') setCount(Number(e.newValue));
  };
  window.addEventListener('storage', handler);
  return () => window.removeEventListener('storage', handler);
}, []);

Рекомендации по архитектуре

  • Для локального состояния, не требующего персистентности, использовать только useState.
  • Для состояния, сохраняемого между сессиями, применять localStorage или sessionStorage.
  • Для сложных приложений можно создать кастомный хук, объединяющий useState и синхронизацию с хранилищем:
function usePersistentState(key, defaultValue) {
  const [state, setState] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : defaultValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(state));
  }, [key, state]);

  return [state, setState];
}

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


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