Перейти к содержанию

Браузерные эффекты и сохранение предпочтений

Публичные страницы «Маршрута» могут формироваться на сервере или заранее при сборке. Однако у читателя есть ещё одно состояние: предпочтение компактного отображения карточек. Оно не является учебным прогрессом и может храниться в конкретном браузере. Добавим переключатель, который сохраняет выбранный вид после загрузки интерфейса.

Результат урока — безопасная работа с браузерным хранилищем при сохранённом SSR. Нам понадобятся $state, $effect и onMount. Предпочтение не синхронизируется между устройствами, не подтверждает личность и не даёт доступ к закладкам. Код и обработка отказа хранилища приведены как ожидаемый пример, который здесь не выполнялся.

Окно появляется после серверного ответа

На сервере нет браузерного window и его localStorage. Даже если компонент позже станет интерактивным, верхние строки script способны участвовать в серверном формировании страницы. Поэтому чтение localStorage непосредственно при объявлении значения превращает допустимую страницу в ошибку серверной среды.

Начальный HTML должен опираться на определённое значение, доступное обеим сторонам. Для компактности выберем false: первая разметка содержит обычные карточки. После появления компонента в браузере прочитаем предпочтение. Такой порядок допускает изменение вида после первого отображения, зато сервер и первый клиентский проход не спорят о неизвестном браузерном значении.

Следующий полный компонент src/lib/components/ViewPreference.svelte оформляет предпочтение отдельно от предметных отметок. Он выводит своё сообщение и передаёт изменение родителю через callback, знакомый по карточке.

<script lang="ts">
  import { onMount } from 'svelte';
  let { onchange }: { onchange: (compact: boolean) => void } = $props();
  let compact = $state(false);
  let ready = $state(false);
  let notice = $state('');
  onMount(() => {
    try { compact = localStorage.getItem('route-compact') === '1'; }
    catch { notice = 'Предпочтение сохранится только до закрытия страницы.'; }
    ready = true;
  });
  $effect(() => {
    if (!ready) return;
    onchange(compact);
    try { localStorage.setItem('route-compact', compact ? '1' : '0'); }
    catch { notice = 'Браузер не разрешил сохранить предпочтение.'; }
  });
</script>
<label><input type="checkbox" bind:checked={compact}> Компактный вид</label>
{#if notice}<p role="status">{notice}</p>{/if}

onMount выполняется после появления компонента в браузере и не выполняется при SSR. $effect также предназначен для побочного взаимодействия в клиентской среде. Это свойства API, проверенные по документации lifecycle hooks и руководству $effect. Наша дополнительная переменная ready выражает уже правило примера: не записывать начальное значение до попытки чтения ранее сохранённого.

Значение из хранилища не является доверенным

В localStorage читатель или расширение браузера может записать произвольную строку. Мы принимаем компактный вид только при точном значении '1'. Другие значения превращаются в false. Здесь не нужен большой JSON-объект, потому что настройка имеет два состояния и ясную версию представления.

Иное правило понадобится для сохранённого массива закладок. Строка JSON может быть повреждена, содержать неизвестные идентификаторы или слишком много элементов. Слепой вызов JSON.parse не превращает результат в корректную модель. В этой серии реальные отметки сохраняются сервером, поэтому браузерное хранилище ограничено предпочтением оформления.

Обратите внимание на catch. Отказ хранения не лишает человека возможности пользоваться страницей. Компактное значение продолжает жить в компоненте; интерфейс лишь сообщает, что оно не переживёт закрытие. Ошибка второстепенной возможности не должна выглядеть как успешное постоянное сохранение, но и не требует скрывать весь каталог.

Побочный эффект и производное состояние

Эффект пишет строку во внешнюю систему — браузерное хранилище. Поэтому он уместен здесь, в отличие от вычисления процента во втором уроке. Процент существует как результат формулы; запись предпочтения имеет действие за пределами реактивных значений. Такое различие помогает выбирать механизм до написания кода.

Эффект читает compact и ready, поэтому изменение переключателя приводит к новой записи. notice в данном эффекте только получает значение и не служит источником зависимости. Если позднее добавить чтение notice и сложную логику сброса, нужно заново проверить, не появился ли цикл изменений. Эффект не должен становиться скрытым посредником между множеством состояний.

В родительской странице добавьте let compact = $state(false) и компонент ViewPreference с onchange={value => compact = value}. Контейнер карточек получает класс через class:compact={compact}. Это фрагменты изменения существующей страницы; каталога, массивов и функции toggle они не заменяют.

<ViewPreference onchange={value => compact = value} />
<div class:compact={compact}>
  <!-- Существующий список LessonCard остаётся внутри этого контейнера. -->
</div>

Ожидаем, что переключение изменит плотность списка, а следующая загрузка в том же браузере восстановит выбор после монтирования. Чтобы эффект было видно, добавьте правило отступов для класса compact; уменьшение размеров кнопок до неудобных значений не входит в задачу компактного вида.

Несколько вкладок и несколько устройств

В текущем компоненте не обрабатывается событие изменения хранилища из другой вкладки. Поэтому переключение в первой вкладке не обязано сразу менять открытую вторую. После её новой загрузки значение будет прочитано. Это ограничение следует назвать, прежде чем обещать «синхронизацию настроек».

Другое устройство имеет другое браузерное хранилище. Если продукт потребует одинакового предпочтения после входа, настройку придётся сохранить в профиле пользователя и решить конфликт с локальным выбором. При этом она останется отдельной от учебного прогресса: восстановление темы или плотности не должно влиять на завершение уроков.

Непостоянное предпочтение и первый кадр

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

Такой перенос не должен превращать cookie предпочтения в доказательство входа. Человек способен установить компактность самостоятельно; сервер может доверять ей только как варианту оформления. Права на изменение прогресса по-прежнему определяются сессией.

Не сохраняйте сложный объект настроек без версии формата. Через несколько месяцев значение может перестать соответствовать новой разметке. Для одного булева поля достаточно договорённости '1' и '0'; расширение потребует явного разбора и допустимых значений. Неизвестное предпочтение должно вести к безопасному обычному виду, а не мешать открытию уроков. Подобная совместимость делает второстепенное состояние устойчивым без усложнения предметной истории.

Проверять пример позднее нужно в трёх условиях: новое хранилище, ранее сохранённая настройка и отказ доступа. Ожидаемый результат в каждом случае описан явно, но здесь ни один из них не подтверждён запуском. Следующая глава перенесёт фильтр каталога в URL, потому что состояние поиска полезно отправлять ссылкой, в отличие от частного предпочтения компактности.

Оглавление серии