useState: общее состояние с SSR

useState — это ref, который безопасен на сервере и переживает гидратацию: общее состояние с ключом, не «протекающее» между пользователями.
Суть: useState создаёт реактивное состояние с ключом, корректно работающее при SSR. Оно сериализуется в payload и восстанавливается на клиенте. В отличие от обычного ref на уровне модуля, useState не разделяет состояние между разными запросами на сервере.

В прошлом уроке мы выяснили: локальный ref в composable даёт каждому вызову своё состояние. А если нужно общее — например, корзина или текущий пользователь, видимые на всех страницах? Наивное решение — объявить ref на уровне модуля, вне функции. В обычном Vue это сработало бы. Но в Nuxt с SSR это опасный баг.

Причина — в природе сервера. На сервере один Node-процесс обслуживает всех пользователей. Модульный ref создаётся один раз и живёт между запросами. Значит, состояние одного пользователя «протечёт» к другому: товары в корзине Алисы увидит Боб. На клиенте такой проблемы нет (у каждого свой браузер), но на сервере это утечка данных.

Решение Nuxt — useState(key, init). Это SSR-безопасное общее состояние: оно привязано к ключу, изолировано на каждый запрос на сервере и автоматически сериализуется в payload для передачи клиенту.

// composables/useCart.ts
export function useCart() {
  const items = useState("cart", () => [])   // ключ "cart"
  function add(name) { items.value.push(name) }
  return { items, add }
}
   Жизнь useState через SSR

   Сервер (запрос Алисы):
     useState("cart") -> свежее [] для ЭТОГО запроса
     add("книга")      -> ["книга"]
     сериализация      -> payload в HTML

   Браузер Алисы:
     читает payload    -> восстанавливает ["книга"]
     гидратация        -> то же состояние, без перезапроса

   Запрос Боба идёт ИЗОЛИРОВАННО: своя пустая корзина

Composables хороши не только как контейнер состояния, но и как способ скрыть сложность за чистым интерфейсом. Представьте composable usePagination: внутри он держит текущую страницу, размер, вычисляет смещение и общее число страниц, а наружу отдаёт лишь page, next, prev и totalPages. Компонент, который его использует, не знает о внутренней арифметике — он просто вызывает методы. Такой подход делает страницы тонкими и декларативными, а логику — тестируемой в изоляции, без необходимости монтировать весь компонент.

Как работает под капотом

Nuxt держит на сервере отдельный контекст для каждого запроса. useState кладёт значение в этот контекст по ключу, поэтому состояния разных пользователей не пересекаются. По завершении рендера всё состояние сериализуется в payload. На клиенте useState с тем же ключом сначала ищет значение в payload — и находит его, не вызывая инициализатор повторно. Так состояние «бесшовно» переходит с сервера в браузер.

Смоделируем разницу между опасным модульным состоянием и изолированным per-request:

// Почему модульный ref течёт, а useState изолирует.
const moduleState = [];                 // ОДИН на все запросы (опасно)

function serverRequest(useStateStore, user, item) {
  // useState: своё хранилище на КАЖДЫЙ запрос
  if (!useStateStore.cart) useStateStore.cart = [];
  useStateStore.cart.push(item);        // изолировано

  moduleState.push(item);               // протекает между всеми
  return {
    isolated: [...useStateStore.cart],
    leaked: [...moduleState],
  };
}

const alice = serverRequest({}, "Алиса", "книга");
const bob   = serverRequest({}, "Боб",   "ручка");

console.log("Алиса isolated:", alice.isolated, "| leaked:", alice.leaked);
console.log("Боб   isolated:", bob.isolated,   "| leaked:", bob.leaked);
console.log("Видно: leaked копит чужие данные, isolated — нет.");

Попробуй сам ▶ — модульное состояние (leaked) копит данные всех «пользователей», а изолированное (isolated) у каждого своё. Именно поэтому в Nuxt общее состояние делают через useState, а не модульный ref.

Частые ошибки

  • Глобальный ref на уровне модуля. На сервере он течёт между пользователями — классическая утечка данных в SSR.
  • Разные ключи для одного состояния. useState("user") и useState("currentUser") — это два разных хранилища.
  • Класть в useState несериализуемое. Функции и классы не переживут payload; храните простые данные.

Best practices

  • Любое общее на всё приложение состояние при SSR — через useState с уникальным ключом.
  • Оборачивайте useState в composable (useCart, useAuth) — единый ключ и чистый API.
  • Для сложного состояния с действиями и геттерами рассмотрите Pinia — официальный стор для Nuxt.

Итог: useState — это SSR-безопасный способ держать общее состояние. Он изолирует данные по запросам и переносит их в браузер через payload. На этом раздел о компонентах завершён — переходим к получению данных через useFetch и useAsyncData.

Проверьте себя
1. Почему обычный ref на уровне модуля опасен при SSR в Nuxt?
AОн работает слишком медленно
BНа сервере один процесс обслуживает всех, и состояние течёт между разными пользователями
Cref нельзя использовать в Nuxt вообще
DОн не поддерживает числа
2. Как useState переносит состояние с сервера на клиент?
AДелает повторный запрос в браузере
BСериализует значение в payload, а клиент восстанавливает его по тому же ключу без повторной инициализации
CСохраняет в localStorage
DСостояние теряется при гидратации