Пару недель назад, когда я писал пост The Hidden Cost of URL Design, мне нужно было добавить подсветку синтаксиса SQL. Я направился на веб-сайт PrismJS, пытаясь вспомнить, можно ли добавить его в качестве плагина. Меня утомило количество вариантов на странице скачивания, поэтому я вернулся к своему коду. Поискав в файле PrismJS, я нашёл в его начале комментарий, содержащий URL:

/* https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+css-extras+markdown+scss+sql&plugins=line-highlight+line-numbers+autolinker */

Я совершенно забыл об этом. После нажатия на URL меня перенаправило на ст��аницу скачивания PrismJS, где все чекбоксы, раскрывающиеся меню и опции были заранее выбраны и полностью соответствовали моей конфигурации. Подобраны темы. Настроены языки. Включены плагины. Всё идеально было воссоздано из этого единственного URL.

Это стало одним из тех моментов, когда ты внезапно снова осознаёшь важность чего-то. Передо мной был URL, не просто указывающий на страницу: он хранил состояние и позволял полностью воссоздать все мои настройки. Не нужна никакая база данных, никакие куки, никакое localStorage. Достаточно одного URL.

Я задумался: как часто мы, фронтенд-разработчики, забываем об URL как об инструменте управления состоянием? Мы пытаемся использовать для этого всевозможные абстракции: глобальные хранилища, контексты, кэши, забывая при этом одну из самых древних и изящных фич веба: скромный URL.

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

Забытая мощь URL

Скотт Хэнселмен однажды произнёс знаменитое «URL — это UI», и в этом он совершенно прав. URL — это не просто технические адреса, которые браузеры используют для получения ресурсов. Это интерфейсы и часть UX.

Но URL — это гораздо больше, чем UI. Это контейнеры состояний. Каждый раз, когда вы создаёте URL, вы принимаете решения о том, какую информацию сохранить, что можно передавать и сохранять в закладки.

Задумайтесь о том, что URL дают нам, не требуя ничего дополнительно:

  • Возможность отправки: отправьте кому-нибудь ссылку, и он увидит ровно то же, что и вы

  • Возможность сохранения в закладки: при сохранении URL вы сохраняете момент во времени

  • История браузера: кнопка «Назад» работает без проблем

  • Глубокие ссылки: можно перемещаться непосредственно к конкретному состоянию приложения

URL делают веб-приложения стабильными и предсказуемыми. Они — первое решение веба для управления состоянием, и надёжно выполняют эту свою задачу в 1991 года. Вопрос не в том, могут ли URL хранить состояние, а в том, используете ли вы весь их потенциал.

Прежде, чем перейти к примерам, давайте разберём, как URL кодируют состояние. Вот типичный URL с состоянием:

Anatomy of URL
Анатомия URL. Источник: What is a URL - MDN Web Docs

Многие годы они считались единственными компонентами URL. Всё поменялось с появлением Text Fragments — фичи, позволяющей ссылаться на конкретный фрагмент текста внутри страницы. Подробнее о ней можно прочитать в моей статье Smarter than ‘Ctrl+F’: Linking Directly to Web Page Content.

Разные части URL кодируют разные типы состояния:

  1. Сегменты пути (/path/to/myfile.html) лучше всего использовать для навигации по иерархическим ресурсам:

    • /users/123/posts — посты пользователя 123

    • /docs/api/authentication — структура документации

    • /dashboard/analytics — разделы приложения

  2. Параметры запросов (?key1=value1&key2=value2) идеально подходят для фильтровопций и конфигурации:

    • ?theme=dark&lang=en— настройки UI

    • ?page=2&limit=20 — пагинация

    • ?status=active&sort=date — фильтрация по дате

    • ?from=2025-01-01&to=2025-12-31 — интервалы дат

  3. Якорь Фрагмент (#SomewhereInTheDocument) идеально подходит для навигации на стороне клиента и разделов страниц:

    • #L20-L35 — выделение строк на GitHub

    • #features — скроллинг к разделу

    • #/dashboard — маршрутизация по одностраничному приложению (однако сегодня она используется редко)

Распространённые паттерны, подходящие для параметров запросов

Множественные значения с разделителями

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

?languages=javascript+typescript+python
?tags=frontend,react,hooks

Вложенные или структурированные данные

Разработчики часто кодируют в одну строку запроса сложные фильтры или объект�� конфигурации. В простом случае используются разделённые запятыми пары ключей и значений, а в других ситуациях сериализуются JSON или даже для безопасности применяется кодировка Base64.

?filters=status:active,owner:me,priority:high
?config=eyJyaWNrIjoicm9sbCJ9==  (закодированный в base64 JSON)

Булевы флаги

В случае флагов или переключателей булевы значения часто передаются в явном виде, или же наличие ключа означает его истинность. Это укорачивает URL и упрощает переключение фич.

?debug=true&analytics=false
?mobile  (присутствие = true)

Массивы (запись с квадратными скобками)

?tags[]=frontend&tags[]=react&tags[]=hooks

Ещё один старый паттерн — это запись с квадратными скобками, описывающая массивы в параметрах запросов. Его первоисточником стали ранние веб-фреймворки наподобие PHP, в которых добавление [] к имени параметра означало, что необходимо сгруппировать несколько значений.

?tags[]=frontend&tags[]=react&tags[]=hooks
?ids[0]=42&ids[1]=73

Многие современные фреймворки и парсеры (наподобие библиотеки qs Node и middleware Express) по-прежнему автоматически распознают этот паттерн. Однако он не стандартизован официально в спецификации URL, поэтому поведение может зависеть от реализации клиента или сервера. Обратите внимание, что на моём веб-сайте он даже ломает подсветку синтаксиса.

Самое важное — это постоянство. Выберите паттерны, подходящие для вашего приложения, и придерживайтесь их.

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

Давайте рассмотрим реальные примеры применения URL в качестве контейнеров состояний:

Конфигурация PrismJS

https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript&plugins=line-numbers

В URL закодирована вся конфигурация подсветки синтаксиса. Если что-то поменять в UI, то URL обновится. Если отправить URL другому пользователю, то он в точности получит ваши настройки. В данном случае используется якорь, а не параметры запроса, но концепция остаётся такой же.

Подсветка строк на GitHub

https://github.com/zepouet/Xee-xCode-4.5/blob/master/XeePhotoshopLoader.m#L108-L136

Ссылка ведёт к конкретному файлу, при этом подсвечивая строки с 108 по 136. Если, например, нажать на эту ссылку в каком-то посте, то вы перейдёте именно к той части кода, о которой говорил автор.

Google Maps

https://www.google.com/maps/@22.443842,-74.220744,19z

Координаты, уровень зума и тип карты — всё это закодировано в URL. Если отправить эту ссылку другу, то он увидит точно то же самое.

Figma и инструменты дизайна

https://www.figma.com/file/abc123/MyDesign?node-id=123:456&viewport=100,200,0.5

До появления ссылок на дизайн, которыми можно делиться, нахождение обновившегося экрана или компонента в большом файле было утомительной задачей. Кому-то нужно было в буквальном смысле показывать, где находятся изменения при помощи скроллинга и зума между слоями. Сегодня ссылка Figma содержит весь контекст наподобие позиции на canvas, уровня зума и выбранного элемента. Буквально всё, что нужно для перехода прямо к нужному рабочему пространству.

Фильтры онлайн-магазинов

https://store.com/laptops?brand=dell+hp&price=500-1500&rating=4&sort=price-asc

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

Паттерны фронтенд-разработки

Прежде, чем мы приступим к подробностям реализации, нужно определиться с чёткой инструкцией о том, что должно попадать в URL. Не всё состояние относится к URL. Вот простая эвристика:

Подходящие кандидаты на попадание в состояние URL:

  • Поисковые запросы и фильтры

  • Пагинация и сортировка

  • Режимы просмотра (список/сетка, тёмный/светлый)

  • Интервалы дат и временные промежутки

  • Выбранные элементы или активные вкладки

  • Конфигурация UI, влияющая на контент

  • Флаги фич и варианты A/B-тестов

Неподходящие кандидаты для состояния URL:

  • Уязвимая информация (пароли, токены, персональная информация)

  • Временные состояния UI (закрытые/открытые модальные окна, развёрнутый раскрывающийся список)

  • Процесс ввода в форму (несохранённые изменения)

  • Крайне большие или сложные вложенные данные

  • Высокочастотные переходные состояния (позиция мыши, позиция скроллинга)

Если вы не уверены, подходит ли какая-то часть состояния для хранения в URL, задайтесь вопросом: если пользователь нажмёт на этот URL, то должен ли он видеть то же самое состояние? Если да, то оно относится к URL. Если нет, выберите другой способ управления состоянием.

Реализация на чистом JavaScript

Благодаря современному API URLSearchParams управлять состоянием в URL очень просто:

// Чтение параметров URL
const params = new URLSearchParams(window.location.search);
const view = params.get('view') || 'grid';
const page = params.get('page') || 1;

// Обновление параметров URL
function updateFilters(filters) {
  const params = new URLSearchParams(window.location.search);

  // Обновление отдельных параметров
  params.set('status', filters.status);
  params.set('sort', filters.sort);

  // Обновление URL без перезагрузки страницы
  const newUrl = `${window.location.pathname}?${params.toString()}`;
  window.history.pushState({}, '', newUrl);

  // Теперь обновляем UI на основании новых фильтров
  renderContent(filters);
}

// Обработка кнопок "назад"/"вперёд"
window.addEventListener('popstate', () => {
  const params = new URLSearchParams(window.location.search);
  const filters = {
    status: params.get('status') || 'all',
    sort: params.get('sort') || 'date'
  };
  renderContent(filters);
});

Событие popstate срабатывает, когда пользователь выполняет навигацию при помощи кнопок браузера «Назад» и «Вперёд». Оно позволяет восстановить UI так, чтобы он соответствовал URL, это необходимо для синхронизации состояния приложения и истории. Обычно этим занимается маршрутизатор фреймворка, но полезно знать, как всё это устроено изнутри.

Реализация с помощью React

React Router и Next.js предоставляют хуки, ещё больше упрощающие работу:

import { useSearchParams } from 'react-router-dom';
// для Next.js 13+: import { useSearchParams } from 'next/navigation';

function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();

  // Чтение из URL (с параметрами по умолчанию)
  const color = searchParams.get('color') || 'all';
  const sort = searchParams.get('sort') || 'price';

  // Обновление URL
  const handleColorChange = (newColor) => {
    setSearchParams(prev => {
      const params = new URLSearchParams(prev);
      params.set('color', newColor);
      return params;
    });
  };

  return (
    <div>
      <select value={color} onChange={e => handleColorChange(e.target.value)}>
        <option value="all">All Colors</option>
        <option value="silver">Silver</option>
        <option value="black">Black</option>
      </select>

      {/* Здесь рендерятся отфильтрованные продукты */}
    </div>
  );
}

Рекомендации по управлению состоянием при помощи URL

Теперь, когда мы увидели, как URL могут хранить состояние приложения, давайте узнаем, как сделать их чистыми, предсказуемыми и удобными для пользователя.

Правильно работаем со значениями по умолчанию

Не засоряйте URL значениями по умолчанию:

// Плохо: URL загрязнён стандартыми значениями
?theme=light&lang=en&page=1&sort=date

// Хорошо: в URL хранятся только нестандартные значения
?theme=dark  // light - значение по умолчанию, так что опускаем его

При чтении параметров используйте в своём коде значения по умолчанию:

function getTheme(params) {
  return params.get('theme') || 'light'; // Обработка в коде значения по умолчанию
}

Устраняем «дребезг» обновлений URL

В случае обновлений с высокой частотой (например, при поиске в процессе ввода) обеспечьте защиту от частых изменений URL:

import { debounce } from 'lodash';

const updateSearchParam = debounce((value) => {
  const params = new URLSearchParams(window.location.search);
  if (value) {
    params.set('q', value);
  } else {
    params.delete('q');
  }
  window.history.replaceState({}, '', `?${params.toString()}`);
}, 300);

// Используем replaceState вместо pushState, чтобы не замусоривать историю

pushState и replaceState

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

URL как контракты

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

Чёткие границы

Хорошо структурированный URL проводит границу между публичным и приватным, клиентом и сервером, тем, чем может делиться пользователь, и что относится к конкретной сессии. Он чётко определяет, где находится состояние и как оно должно себя вести. Разработчики при этом знают, что можно безопасно хранить, пользователи знают, что можно сохранять в закладки, а машины — что стоит индексировать.

В этом смысле URL действуют в качестве интерфейсов: видимых, предсказуемых и стабильных.

Передача смысла

Хорошо читаемые URL объясняют себя. Рассмотрим разницу между двумя URL.

https://example.com/p?id=x7f2k&v=3
https://example.com/products/laptop?color=silver&sort=price

Первый скрывает своё предназначение. Второй рассказывает историю. Человек может прочитать её и понять, что перед ним. Машина может спарсить его и извлечь осмысленную структуру.

Джим Нильсен называет это «примерами отличных URL». URL, которые объясняют себя сами.

Кэширование и производительность

URL — это ключи кэша. Хорошо спроектированные URL позволяют использовать более совершенные стратегии кэширования:

  • Тот же URL = тот же ресурс = попадание в кэш

  • Параметры запросов определяют вариации кэша

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

Можно даже визуализировать путь пользователя без лишнего кода отслеживания:

Инструмент аналитики может отследить этот поток без дополнительного инструментария. Каждый параметр URL становится размерностью, которую можно анализировать.

Версионность и эволюция

URL могут сообщать о версиях API, флагах фич и экспериментах:

?v=2                   // Версия API
?beta=true             // Фичи беты
?experiment=new-ui     // Вариант A/B-теста

Это сильно повышает удобство постепенного развёртывания и обеспечения обратной совместимости.

Антипаттерны

Даже при благих намерениях можно использовать состояние URL неправильно. Вот одни из самых распространённых ошибок:

SPA с «состоянием только в памяти»

Классическая ошибка в одностраничном приложении:

// Пользователь нажимает на "Обновить" и теряет всё
const [filters, setFilters] = useState({});

Если ваше приложение теряет своё состояние при обновлении, то вы ломаете одну из фундаментальных фич веба. Пользователи ожидают, что URL сохранят контекст. Вспоминается видео, ставшее популярным много лет назад, где пользовательница Reddit жаловалась на сайт онлайн-магазина: при каждом нажатии на кнопку «Назад» её фильтры терялись. Если пользователи теряют контекст, то теряют и терпение.

Уязвимые данные в URL

Это кажется очевидным, но всё же стоит повторить:

// НИКОГДА ТАК НЕ ДЕЛАЙТЕ
?password=secret123

URL записываются в куче мест: в истории браузера, логах серверов, аналитике, заголовках referrer. Обращайтесь с ними, как с публичной информацией.

Несогласованное или непонятное именование

// Непонятные и несогласованные имена
?foo=true&bar=2&x=dark

// Самодокументирующиеся и согласованные имена
?mobile=true&page=2&theme=dark

Выбирайте логичные имена параметров. Будущий вы (и ваша команда) будет вам благодарен.

Перегрузка URL сложными состояниями

?config=eyJtZXNzYWdlIjoiZGlkIHlvdSByZWFsbHkgdHJpZWQgdG8gZGVjb2RlIHRoYXQ_IiwiZmlsdGVycyI6eyJzdGF0dXMiOlsiYWN0aXZlIiwicGVuZGluZyJdLCJwcmlvcml0eSI6WyJoaWdoIiwibWVkaXVtIl0sInRhZ3MiOlsiZnJvbnRlbmQiLCJyZWFjdCIsImhvb2tzIl0sInJhbmdlIjp7ImZyb20iOiIyMDI0LTAxLTAxIiwidG8iOiIyMDI0LTEyLTMxIn19LCJzb3J0Ijp7ImZpZWxkIjoiY3JlYXRlZEF0Iiwib3JkZXIiOiJkZXNjIn0sInBhZ2luYXRpb24iOnsicGFnZSI6MSwibGltaXQiOjIwfX0==

Если вам нужно закодировать в base64 огромный JSON-объект, то URL, вероятно — не лучшее место для хранения этого состояния.

Ограничения длины URL

Браузеры и серверы накладывают практические ограничения на длину URL (обычно где-то от 2000 до 8000 символов), но в реальности нюансов гораздо больше. Как объяснено в подробном ответе на Stack Overflow, ограничения возникают из-за сочетания поведения браузера, конфигураций серверов, CDN и даже ограничений поисковых движков. Если вы упираетесь в эти ограничения, значит, пора переосмыслить свой подход.

Поломка кнопки «Назад»

// Некорректная замена состояния
history.replaceState({}, '', newUrl); // Здесь нужно использовать pushState

Уважайте историю браузера. Если действие пользователя должно быть отменяемым через кнопку «Назад», то используйте pushState. Если это доработка, выберите replaceState.

В заключение

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

Мы создаём всё более сложные библиотеки управления состоянием наподобие Redux, MobX, Zustand, Recoil и других. Все они полезны, но иногда лучшим решением оказывается то, которое всегда было с нами.

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

Комментарии (1)


  1. Atorian
    04.11.2025 07:22

    Делаю так с незапамятных времен и боли ин мемори стейта не знаю. Всем рекомендую.