# Согласие на файлы cookie в одностраничных приложениях

> Согласие на cookie в React, Vue и других SPA: общее состояние согласия, учет смены маршрутов, гидратация при SSR, блокировка встраиваний и мгновенный отзыв.

Source: https://www.cookiesh.eu/ru/blog/consent-in-single-page-apps  
Published: 2026-08-11

## Key takeaways

- В одностраничном приложении согласие — это состояние приложения: храните его в одном месте, делитесь им со всеми компонентами и реагируйте на изменения без перезагрузки.
- По умолчанию устанавливайте для каждой необязательной категории отказ, чтобы код, выполняющийся до загрузки сохраненного выбора, не мог допустить утечку данных.
- Отправляйте виртуальные просмотры страниц при смене маршрута, только если посетитель согласился на аналитику.
- Считывайте файл cookie согласия при серверном рендеринге или отрисовывайте баннер только на клиенте, чтобы избежать несоответствий при гидратации и мерцания баннера.
- Отозвать согласие должно быть так же легко, как дать его, а перезагрузка страницы часто оказывается самым чистым способом остановить уже работающие скрипты.

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

## Почему в одностраничном приложении с согласием на файлы cookie сложнее?

Большинство схем работы с согласием рассчитаны на полную загрузку страниц, а SPA загружается один раз и затем переключает представления с помощью JavaScript. Это ломает четыре допущения:

- **Скрипты загружаются один раз.** Если аналитика была заблокирована при загрузке, а посетитель согласился позже, никакая загрузка страницы ее не подхватит. Включать ее придется на месте.
- **Просмотры страниц виртуальные.** Смена маршрута не перезагружает страницу, поэтому о просмотрах должен сообщать роутер — и сначала он должен проверять согласие.
- **Выбор меняется посреди сеанса.** Посетитель может согласиться, походить по сайту, а затем отозвать согласие по ссылке в подвале, когда скрипты уже работают в памяти.
- **Серверный рендеринг добавляет гидратацию.** Во фреймворках вроде Next.js или Nuxt HTML с сервера должен совпадать с первой отрисовкой на клиенте, но сервер может не знать, какой выбор сделал посетитель.

## Как моделировать состояние согласия в SPA?

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

```ts
// consent-store.ts
export type Category = 'necessary' | 'preferences' | 'analytics' | 'marketing';
export type Consent = Record<Category, boolean>;

export const DENIED: Consent = {
  necessary: true, preferences: false, analytics: false, marketing: false,
};

let state: Consent = DENIED;
const listeners = new Set<() => void>();

export const consentStore = {
  get: (): Consent => state,
  set(next: Partial<Consent>) {
    state = { ...state, ...next, necessary: true };
    listeners.forEach((notify) => notify());
  },
  subscribe(listener: () => void) {
    listeners.add(listener);
    return () => { listeners.delete(listener); };
  },
};
```

В реальном приложении `set` также сохранял бы выбор в собственном (first-party) файле cookie, записывал бы его в журнал согласий и обновлял бы Google Consent Mode с помощью `gtag('consent', 'update', …)`. В React хук `useSyncExternalStore` связывает хранилище с компонентами, и тот же хук может блокировать встраиваемый контент, который устанавливает файлы cookie:

```tsx
// consent.tsx
import { useSyncExternalStore } from 'react';
import { consentStore, DENIED } from './consent-store';

export function useConsent() {
  return useSyncExternalStore(consentStore.subscribe, consentStore.get, () => DENIED);
}

export function VideoEmbed({ videoId }: { videoId: string }) {
  const { marketing } = useConsent();

  if (!marketing) {
    return (
      <div className="embed-placeholder">
        <p>Это видео размещено на YouTube, который может устанавливать маркетинговые файлы cookie.</p>
        <button type="button" onClick={() => consentStore.set({ marketing: true })}>
          Разрешить маркетинговые файлы cookie и воспроизвести
        </button>
      </div>
    );
  }

  return (
    <iframe
      src={`https://www.youtube-nocookie.com/embed/${videoId}`}
      title="Видео о продукте"
      allowFullScreen
    />
  );
}
```

Третий аргумент `useSyncExternalStore` — это снимок состояния, который используется при серверном рендеринге и гидратации. Если возвращать там `DENIED`, сервер и первая отрисовка на клиенте совпадут, а сразу после гидратации React перерисует компонент с реальным состоянием. Во Vue ту же роль играет composable-функция, оборачивающая реактивный `ref`.

На скрипты, которые не являются компонентами, подписывайтесь напрямую через хранилище и загружайте их динамическим `import()`, как только для их категории дано согласие. Схемы активации описаны в нашем руководстве о том, [как блокировать скрипты до получения согласия](/ru/blog/block-scripts-until-consent).

## Как отслеживать смену маршрутов только при наличии согласия?

Отправляйте виртуальный просмотр страницы из роутера после каждого перехода и проверяйте согласие в этот момент. Если посетитель дает согласие на аналитику посреди сеанса, разумно зафиксировать страницу, на которой он находится, ведь именно с нее начинается действие согласия.

```tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useConsent } from './consent';
import { trackPageView } from './analytics';

export function PageViewTracker() {
  const { pathname } = useLocation();
  const { analytics } = useConsent();

  useEffect(() => {
    if (analytics) trackPageView(pathname);
  }, [pathname, analytics]);

  return null;
}
```

Следите за двойным учетом. Некоторые инструменты аналитики умеют автоматически отправлять просмотры страниц при изменениях истории браузера; в Google Analytics 4 это доступно как параметр улучшенной статистики. Используйте либо автоматический вариант, либо собственный трекер, но не оба сразу.

## Как избежать несоответствий при гидратации и мерцания баннера?

Решите, где выбор посетителя будет известен при первой отрисовке. Есть два надежных варианта:

- **Считывайте файл cookie согласия на сервере.** Файлы cookie отправляются с каждым запросом, поэтому страницы с серверным рендерингом могут корректно показать или скрыть баннер с первого же байта. Это работает, только если HTML не кешируется и не используется совместно разными посетителями.
- **Отрисовывайте баннер только на клиенте.** Для статически сгенерированных или кешируемых в CDN страниц ничего не отрисовывайте на сервере и показывайте баннер после монтирования приложения. Размещайте его как фиксированный оверлей, чтобы позднее появление не вызывало сдвига макета; см. статью [баннеры cookie и Core Web Vitals](/ru/blog/cookie-banners-core-web-vitals).

В любом случае файл cookie или запись в хранилище, запоминающая выбор, как правило, считается необходимой для соблюдения этого выбора; см. статью [какие файлы cookie считаются строго необходимыми](/ru/blog/strictly-necessary-cookies).

## Как обрабатывать отзыв согласия посреди сеанса?

Сделайте отзыв мгновенным и таким же простым, как предоставление согласия, — этого требует статья 7(3) Общего регламента по защите данных (GDPR). На практике это означает постоянную ссылку «Настройки cookie», обычно в подвале сайта, которая снова открывает настройки без загрузки страницы. Когда посетитель отзывает согласие для категории:

1. Обновите хранилище и сохраните новый выбор.
2. Если вы используете теги Google, отправьте `gtag('consent', 'update', …)` с соответствующими сигналами в значении `'denied'`.
3. Прекратите вызывать функции отслеживания; код, зависящий от хранилища, делает это автоматически.
4. Удалите собственные файлы cookie, установленные отозванной категорией, если вам известны их имена и домены. Ваши скрипты не могут удалить файлы cookie, установленные на других доменах.
5. Перезагрузите страницу, если какой-либо из отозванных скриптов уже работает. Библиотеки хранят таймеры, обработчики событий и идентификаторы в памяти, и перезагрузка — единственный надежный способ от них избавиться.

Перезагрузка после отзыва согласия — разумный компромисс: это случается редко, а альтернатива — выполнять код, на запуск которого у вас больше нет разрешения.

## Итоги

В SPA согласие — это состояние: по умолчанию — отказ, одно общее хранилище, скрипты и встраиваемый контент зависят от него, просмотры страниц отправляются только при наличии согласия, а отзыв вступает в силу сразу. Такие инструменты, как Cookiesh, могут взять на себя большую часть этой связующей работы: React SDK предоставляет `CookieshProvider` и хук `useConsent()`, а для всего остального есть событие `consent:updated`.

*Эта статья носит общий информационный характер и не является юридической консультацией. Для решений, касающихся вашей конкретной ситуации, обратитесь к квалифицированному специалисту по защите данных.*

## FAQ

### Нужно ли снова показывать баннер cookie после каждой смены маршрута?

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

### Можно ли хранить выбор согласия в localStorage, а не в файле cookie?

В браузере работают оба варианта, но файл cookie еще и отправляется на ваш сервер с каждым запросом, поэтому страницы с серверным рендерингом могут показать правильное состояние уже при первой отрисовке. localStorage доступен только клиентскому коду после загрузки приложения.

### Обрабатывают ли согласие за меня компоненты скриптов во фреймворках, например next/script?

Нет. Такие вспомогательные средства, как компонент Script в Next.js, управляют тем, когда скрипт загружается относительно гидратации, а не тем, согласился ли на него посетитель. Отрисовывайте их в зависимости от состояния согласия, чтобы они не добавлялись на страницу до того, как посетитель даст согласие.

## Sources

- [Directive 2002/58/EC (ePrivacy Directive), EUR-Lex](https://eur-lex.europa.eu/eli/dir/2002/58/oj)
- [Regulation (EU) 2016/679 (GDPR), EUR-Lex](https://eur-lex.europa.eu/eli/reg/2016/679/oj)
- [Google: обзор режима согласия](https://developers.google.com/tag-platform/security/guides/consent)
