Паттерны работы с Redux

React Router используется для управления навигацией в приложениях на React, а Redux — для централизованного хранения состояния. Совместное использование этих библиотек позволяет строить сложные интерфейсы с синхронизированным состоянием URL и состояния приложения.

Подключение React Router к Redux осуществляется через библиотеку connected-react-router, которая обеспечивает синхронизацию истории браузера с хранилищем Redux. Для этого создается объект истории:

import { createBrowserHistory } from 'history';

export const history = createBrowserHistory();

Далее при конфигурации Redux хранилища используется routerMiddleware и connectRouter:

import { applyMiddleware, createStore, combineReducers } from 'redux';
import { routerMiddleware, connectRouter } from 'connected-react-router';
import { history } from './history';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

const store = createStore(
  combineReducers({
    router: connectRouter(history),
    ...rootReducer
  }),
  applyMiddleware(
    routerMiddleware(history),
    thunk
  )
);

export default store;

Этот подход позволяет следить за изменениями URL через Redux и управлять навигацией с помощью действий.


Использование <Route> и <Switch> в связке с Redux

Компоненты React Router, такие как <Route> и <Switch>, могут получать данные из Redux через connect или useSelector. Например, условный рендеринг страниц в зависимости от состояния:

import { useSelector } from 'react-redux';
import { Route, Switch, Redirect } from 'react-router-dom';
import HomePage from './HomePage';
import Dashboard from './Dashboard';

function AppRoutes() {
  const isAuthenticated = useSelector(state => state.auth.isAuthenticated);

  return (
    <Switch>
      <Route exact path="/" component={HomePage} />
      <Route
        path="/dashboard"
        render={() =>
          isAuthenticated ? <Dashboard /> : <Redirect to="/" />
        }
      />
    </Switch>
  );
}

Ключевой момент: использование render вместо component позволяет передавать данные из Redux напрямую в маршрут.


Паттерн “Redux-driven navigation”

Иногда нужно, чтобы навигация управлялась исключительно через Redux. Для этого создаются специальные действия:

import { push } from 'connected-react-router';

export const navigateTo = (path) => (dispatch) => {
  dispatch(push(path));
};

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


Синхронизация состояния формы с маршрутом

Часто требуется сохранять в URL параметры формы или фильтров. Комбинация Redux и React Router позволяет это сделать следующим образом:

import { useSelector, useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { push } from 'connected-react-router';

function FilterComponent() {
  const dispatch = useDispatch();
  const location = useLocation();
  const filters = useSelector(state => state.filters);

  const updateFilter = (newFilter) => {
    dispatch({ type: 'UPDATE_FILTER', payload: newFilter });
    const searchParams = new URLSearchParams(location.search);
    searchParams.set('filter', newFilter);
    dispatch(push(`${location.pathname}?${searchParams.toString()}`));
  };

  return (
    <input
      value={filters.current}
      onCha nge={(e) => updateFilter(e.target.value)}
    />
  );
}

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


Lazy-loading маршрутов с Redux

Для улучшения производительности большие страницы загружаются динамически. Redux и React Router совместно поддерживают динамическое подключение редьюсеров и маршрутов:

import { Suspense, lazy } from 'react';
import { Route, Switch } from 'react-router-dom';

const Dashboard = lazy(() => import('./Dashboard'));
const Reports = lazy(() => import('./Reports'));

function AppRoutes() {
  return (
    <Suspense fallback={<div>Загрузка...</div>}>
      <Switch>
        <Route path="/dashboard" component={Dashboard} />
        <Route path="/reports" component={Reports} />
      </Switch>
    </Suspense>
  );
}

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


Паттерн “Route-based state slices”

Для крупных приложений рекомендуется разделять Redux-состояние по маршрутам. Каждый маршрут имеет собственный slice, который подключается только при активации страницы:

import { combineReducers } from 'redux';
import dashboardReducer from './dashboardReducer';
import reportsReducer from './reportsReducer';

export const createRootReducer = (injectedReducers = {}) =>
  combineReducers({
    ...injectedReducers
  });

При переходе на маршрут /dashboard подключается dashboardReducer, при переходе на /reportsreportsReducer. Это улучшает масштабируемость и уменьшает размер глобального состояния.


Взаимодействие Redux middleware и React Router

Middleware позволяют перехватывать действия Redux для реализации сложной логики навигации. Пример: редирект после успешного логина:

const authMiddleware = ({ dispatch }) => next => action => {
  if (action.type === 'LOGIN_SUCCESS') {
    dispatch(push('/dashboard'));
  }
  return next(action);
};

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


Итоговые принципы

  • Использование connected-react-router для синхронизации истории с Redux.
  • Условная навигация через render и Redirect.
  • Управление URL через Redux-действия (push, replace).
  • Синхронизация состояния формы и фильтров с URL.
  • Lazy-loading маршрутов и динамическое подключение редьюсеров.
  • Разделение состояния на route-based slices для масштабируемости.
  • Middleware для централизованной навигационной логики.

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