Синхронизация состояния

Состояние в одностраничных приложениях (SPA) отражает текущий набор данных, от которого зависит отображение интерфейса и поведение приложения. В контексте маршрутизации Page.js состояние включает:

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

Page.js не предоставляет встроенного менеджера состояния, однако он активно участвует в его синхронизации, поскольку маршруты напрямую связаны с URL — одним из ключевых источников состояния.

Связь URL и состояния

URL в SPA выступает как источник истины для навигационного состояния. В Page.js это реализуется через обработчики маршрутов, которые получают объект ctx:

page('/user/:id', (ctx) => {
  console.log(ctx.params.id);
});

Здесь ctx.params.id — часть состояния, извлечённая из URL. Синхронизация заключается в том, чтобы:

  • обновлять URL при изменении состояния
  • восстанавливать состояние при загрузке URL

Основные элементы ctx

  • ctx.path — полный путь
  • ctx.params — параметры маршрута
  • ctx.querystring — строка запроса
  • ctx.state — пользовательское состояние (History API)

Синхронизация через History API

Page.js использует History API браузера (pushState, replaceState) для управления навигацией без перезагрузки страницы.

Передача состояния

page('/profile', (ctx) => {
  console.log(ctx.state);
});

page.show('/profile', { user: 'Alex' });

Здесь второй аргумент page.show() становится частью ctx.state.

Особенности

  • состояние не отображается в URL
  • сохраняется в истории браузера
  • доступно при навигации назад/вперёд

Хранение глобального состояния

Page.js не управляет состоянием приложения, поэтому используются внешние решения:

  • простые объекты
  • EventEmitter-подход
  • библиотеки: Redux, Zustand, Vuex (в зависимости от стека)

Пример с глобальным объектом

const store = {
  user: null
};

page('/login', (ctx) => {
  store.user = { name: 'Alex' };
  page.redirect('/dashboard');
});

page('/dashboard', (ctx) => {
  console.log(store.user);
});

Недостаток — отсутствие реактивности и централизованного контроля.

Синхронизация асинхронных данных

Маршруты часто требуют загрузки данных с сервера. Важно синхронизировать:

  • момент загрузки
  • отображение
  • состояние загрузки

Middleware-подход

Page.js поддерживает цепочку middleware:

function loadUser(ctx, next) {
  fetch(`/api/user/${ctx.params.id}`)
    .then(res => res.json())
    .then(user => {
      ctx.user = user;
      next();
    });
}

page('/user/:id', loadUser, (ctx) => {
  renderUser(ctx.user);
});

Здесь состояние (ctx.user) синхронизируется до рендеринга.

Проблема гонок (race conditions)

Если пользователь быстро переключает маршруты:

  • старый запрос может завершиться позже нового
  • интерфейс покажет устаревшие данные

Решение:

  • использовать AbortController
  • проверять актуальность маршрута
let currentRequest = null;

function loadUser(ctx, next) {
  if (currentRequest) currentRequest.abort();

  const controller = new AbortController();
  currentRequest = controller;

  fetch(`/api/user/${ctx.params.id}`, { signal: controller.signal })
    .then(res => res.json())
    .then(user => {
      ctx.user = user;
      next();
    })
    .catch(err => {
      if (err.name !== 'AbortError') throw err;
    });
}

Синхронизация UI и маршрутов

UI должен отражать текущее состояние маршрута:

  • активные ссылки
  • вкладки
  • фильтры

Активные ссылки

page('*', (ctx, next) => {
  document.querySelectorAll('a').forEach(link => {
    link.classList.toggle('active', link.pathname === ctx.path);
  });
  next();
});

Состояние фильтров через query string

page('/products', (ctx) => {
  const params = new URLSearchParams(ctx.querystring);
  const category = params.get('category');

  renderProducts(category);
});

Обновление состояния:

page.show('/products?category=books');

Двусторонняя синхронизация

Состояние должно обновляться в обе стороны:

  1. Из URL → в приложение
  2. Из приложения → в URL

Пример

function updateFilter(category) {
  page.show(`/products?category=${category}`);
}

Теперь:

  • изменение фильтра обновляет URL
  • перезагрузка страницы сохраняет состояние

Работа с вложенным состоянием

Для сложных интерфейсов (например, dashboard) состояние может быть составным:

/dashboard?tab=analytics&range=month

Обработка:

page('/dashboard', (ctx) => {
  const params = new URLSearchParams(ctx.querystring);

  const tab = params.get('tab') || 'overview';
  const range = params.get('range') || 'week';

  renderDashboard({ tab, range });
});

Кэширование состояния

Повторные переходы между маршрутами могут требовать повторной загрузки данных. Для оптимизации используется кэш.

const cache = {};

function loadUser(ctx, next) {
  const id = ctx.params.id;

  if (cache[id]) {
    ctx.user = cache[id];
    return next();
  }

  fetch(`/api/user/${id}`)
    .then(res => res.json())
    .then(user => {
      cache[id] = user;
      ctx.user = user;
      next();
    });
}

Синхронизация с формами

Формы могут изменять состояние маршрута:

document.querySelector('#search').addEventListener('submit', (e) => {
  e.preventDefault();

  const query = e.target.elements.q.value;
  page.show(`/search?q=${encodeURIComponent(query)}`);
});

Обработчик:

page('/search', (ctx) => {
  const params = new URLSearchParams(ctx.querystring);
  const q = params.get('q');

  renderSearch(q);
});

Управление переходами и состоянием

Иногда требуется предотвратить переход при несохранённых данных.

let isDirty = true;

page('/edit', (ctx, next) => {
  if (isDirty && !confirm('Есть несохранённые изменения')) {
    return;
  }
  next();
});

Синхронизация с компонентными фреймворками

При использовании React, Vue или Svelte Page.js обычно интегрируется как слой маршрутизации.

Пример с React

page('/user/:id', (ctx) => {
  ReactDOM.render(<User id={ctx.params.id} />, root);
});

Состояние:

  • хранится в компонентах
  • синхронизируется через props из маршрута

Восстановление состояния при перезагрузке

Так как URL — основной источник состояния, при перезагрузке:

  • Page.js повторно вызывает маршрут
  • состояние восстанавливается автоматически

Однако ctx.state (History API) не сохраняется между перезагрузками. Для этого используется:

  • localStorage
  • sessionStorage
localStorage.setItem('user', JSON.stringify(user));

const user = JSON.parse(localStorage.getItem('user'));

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

Распространённые проблемы:

  • рассинхронизация URL и UI
  • устаревшие данные
  • потеря состояния при навигации
  • гонки запросов

Подходы к решению

  • централизованное управление состоянием
  • строгая связь URL ↔︎ состояние
  • контроль асинхронных операций
  • использование middleware

Паттерны проектирования

Route-driven state

Состояние полностью определяется маршрутом:

  • URL — единственный источник истины
  • удобно для SEO и восстановления

Hybrid state

  • часть состояния в URL
  • часть — в памяти

Пример:

  • /products?page=2 — в URL
  • выбранные товары — в памяти

Ephemeral state

  • временное состояние
  • не сохраняется в URL

Пример:

  • открытые модальные окна
  • временные уведомления

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

  • хранить навигационное состояние в URL
  • использовать query string для фильтров
  • избегать избыточного ctx.state
  • контролировать асинхронные запросы
  • применять middleware для подготовки данных
  • кэшировать часто используемые данные

Синхронизация состояния в Page.js строится вокруг идеи: URL управляет приложением, а маршруты становятся точками консолидации данных, интерфейса и логики.