Синхронизация URL с глобальным состоянием

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


Использование useLocation и useNavigate для синхронизации

useLocation предоставляет объект location, содержащий текущий путь, поисковую строку и hash. Это позволяет наблюдать за изменениями URL и использовать их для обновления глобального состояния.

Пример:

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useStore } from './store';

function URLSync() {
  const location = useLocation();
  const { setFilter } = useStore();

  useEffect(() => {
    const params = new URLSearchParams(location.search);
    const filter = params.get('filter') || '';
    setFilter(filter);
  }, [location.search, setFilter]);

  return null;
}

Здесь любое изменение поисковой строки в URL автоматически обновляет глобальное состояние.

useNavigate позволяет программно изменять URL, что полезно при обновлении состояния через действия пользователя:

import { useNavigate } from 'react-router-dom';
import { useStore } from './store';

function FilterComponent() {
  const navigate = useNavigate();
  const { filter } = useStore();

  const updateFilter = (newFilter) => {
    navigate(`?filter=${encodeURIComponent(newFilter)}`, { replace: true });
  };

  return (
     updateFilter(e.target.value)}
      placeholder="Введите фильтр"
    />
  );
}

Синхронизация через глобальный стейт (Redux, Zustand, Recoil)

Для сложных приложений рекомендуется использовать глобальное состояние. Это позволяет единожды определить логику синхронизации URL и состояния и переиспользовать её во всех компонентах.

Пример с Redux:

import { createSlice } from '@reduxjs/toolkit';

const filterSlice = createSlice({
  name: 'filter',
  initialState: { value: '' },
  reducers: {
    setFilter: (state, action) => {
      state.value = action.payload;
    },
  },
});

export const { setFilter } = filterSlice.actions;
export default filterSlice.reducer;

Компонент синхронизации:

import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { setFilter } from './filterSlice';

function URLSync() {
  const location = useLocation();
  const navigate = useNavigate();
  const dispatch = useDispatch();
  const filter = useSelector((state) => state.filter.value);

  useEffect(() => {
    const params = new URLSearchParams(location.search);
    const urlFilter = params.get('filter') || '';
    if (urlFilter !== filter) {
      dispatch(setFilter(urlFilter));
    }
  }, [location.search, dispatch, filter]);

  useEffect(() => {
    const params = new URLSearchParams(location.search);
    if (filter !== params.get('filter')) {
      params.set('filter', filter);
      navigate(`?${params.toString()}`, { replace: true });
    }
  }, [filter, location.search, navigate]);

  return null;
}

Такой подход обеспечивает двустороннюю синхронизацию, предотвращая рассинхронизацию состояния и URL.


Использование searchParams для более чистого кода

React Router v6 предоставляет хук useSearchParams, который упрощает работу с параметрами запроса:

import { useSearchParams } from 'react-router-dom';
import { useStore } from './store';

function FilterSearch() {
  const [searchParams, setSearchParams] = useSearchParams();
  const { filter, setFilter } = useStore();

  useEffect(() => {
    const param = searchParams.get('filter') || '';
    if (param !== filter) setFilter(param);
  }, [searchParams, filter, setFilter]);

  const handleChange = (value) => {
    setFilter(value);
    setSearchParams({ filter: value });
  };

  return  handleChange(e.target.value)} />;
}

Преимущество useSearchParams в том, что нет необходимости вручную формировать URL, React Router автоматически поддерживает актуальность строки запроса.


Важные нюансы синхронизации

  • Избегать бесконечных циклов: при двусторонней синхронизации состояние и URL могут постоянно обновлять друг друга. Использование проверки на изменение значения предотвращает лишние вызовы.
  • Использовать replace при обновлении URL: это предотвращает засорение истории браузера промежуточными состояниями.
  • Сохранять сериализацию сложных данных: если состояние сложное (объекты, массивы), рекомендуется использовать JSON и кодировать результат через encodeURIComponent.
setSearchParams({ data: encodeURIComponent(JSON.stringify(state)) });
  • Поддержка кэширования и истории: при работе с глобальным состоянием стоит учитывать, что переходы по истории браузера должны корректно восстанавливать состояние из URL, иначе пользователь может столкнуться с рассинхронизацией.

Реактивная синхронизация с помощью эффекта зависимости

Использование useEffect с зависимостями от URL и состояния обеспечивает реактивную синхронизацию, когда изменения в одном источнике автоматически отражаются в другом. Это ключ к поддержке консистентного состояния приложения, особенно при совместном использовании с Redux или Zustand.


Рекомендации по структуре приложения

  1. Выделить отдельный компонент синхронизации: один компонент отвечает за связь URL и глобального состояния, чтобы не дублировать логику.
  2. Минимизировать вложенные зависимости: сложные зависимости в useEffect могут привести к непредсказуемым обновлениям.
  3. Использовать утилиты для работы с параметрами: URLSearchParams, query-string или встроенные методы React Router упрощают чтение и запись параметров.