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.