Изоляция состояния в layouts

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


Layout как контейнер состояния

Layout-компонент в TanStack Router выступает контейнером для маршрутов, предоставляя возможность хранить состояние, которое будет доступно всем дочерним маршрутам. Это состояние может включать:

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

Пример простого layout-компонента с локальным состоянием:

import { useState } from 'react';
import { Outlet } from '@tanstack/react-router';

function DashboardLayout() {
  const [theme, setTheme] = useState('light');
  
  return (
    <div className={`dashboard ${theme}`}>
      <header>
        <button onCl ick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
          Переключить тему
        </button>
      </header>
      <Outlet />
    </div>
  );
}

export default DashboardLayout;

В этом примере theme хранится на уровне layout и доступен для всех дочерних маршрутов через контекст или пропсы. Каждое изменение состояния влияет только на этот layout и его потомков, не затрагивая другие части приложения.


Использование useLocalLoader и useLocalMutation

Для более сложных сценариев, когда состояние должно быть асинхронным и сохраняться между переходами, TanStack Router предоставляет локальные загрузчики и мутации (useLocalLoader, useLocalMutation).

Пример асинхронного состояния на layout:

import { useLocalLoader } from '@tanstack/react-router';

function ProjectsLayout() {
  const projects = useLocalLoader({
    key: 'projects',
    loader: async () => {
      const response = await fetch('/api/projects');
      return response.json();
    },
  });

  return (
    <div>
      <h2>Проекты</h2>
      {projects.isLoading && <p>Загрузка...</p>}
      {projects.data?.map(project => (
        <div key={project.id}>{project.name}</div>
      ))}
    </div>
  );
}

Преимущества подхода:

  • Состояние кешируется только для данного layout.
  • Нет необходимости хранить данные глобально в Redux или Context.
  • Автоматическое управление статусами загрузки (isLoading, error).

Изоляция состояния между ветками

В TanStack Router можно создавать несколько layout-уровней, каждый со своим локальным состоянием. Например:

const rootRoute = createRoute({
  path: '/',
  component: RootLayout,
  children: [
    {
      path: 'dashboard',
      component: DashboardLayout,
      children: [
        { path: 'analytics', component: AnalyticsPage },
        { path: 'settings', component: SettingsPage },
      ],
    },
    {
      path: 'projects',
      component: ProjectsLayout,
      children: [
        { path: ':projectId', component: ProjectDetailPage },
      ],
    },
  ],
});

В такой структуре:

  • DashboardLayout управляет своим локальным состоянием (например, переключением темы, фильтрами данных аналитики);
  • ProjectsLayout имеет отдельное локальное состояние (список проектов, состояние загрузки).

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


Контекст и проброс состояния

Для передачи состояния вниз по иерархии TanStack Router рекомендует использовать React Context. Например, если необходимо, чтобы дочерние страницы имели доступ к локальному состоянию layout:

import { createContext, useContext } from 'react';

const DashboardContext = createContext(null);

function DashboardLayout() {
  const [theme, setTheme] = useState('light');

  return (
    <DashboardContext.Provider value={{ theme, setTheme }}>
      <Outlet />
    </DashboardContext.Provider>
  );
}

function useDashboard() {
  return useContext(DashboardContext);
}

Дочерние страницы могут получить доступ к состоянию через useDashboard(), не затрагивая глобальный store.


Важные практики

  1. Минимизировать глобальное состояние: использовать локальные состояния layout вместо Redux или Context для разделов приложения.
  2. Разделять логику: каждый layout отвечает за свои данные и UI-настройки.
  3. Кешировать данные локально: useLocalLoader и useLocalMutation позволяют хранить состояние на уровне layout, ускоряя загрузку страниц.
  4. Избегать нежеланных сайд-эффектов: изоляция состояния предотвращает случайное изменение данных в других маршрутах.

Особенности при навигации

TanStack Router автоматически сохраняет состояние layout при навигации между дочерними маршрутами, если layout не размонтируется. Если же навигация приводит к размонтированию layout, состояние теряется, что можно использовать для “сброса” данных при переходе между разделами. Для сохранения состояния при размонтировании можно использовать внешние стораджи или глобальный store.


Примеры реального использования

  • Панель администратора: каждый раздел (Dashboard, Users, Projects) имеет свой layout с фильтрами, локальными настройками таблиц и кешированными данными.
  • Интернет-магазин: каталог товаров, корзина и профиль пользователя могут быть отдельными layout с изолированным состоянием, чтобы фильтры и временные данные корзины не мешали друг другу.
  • Сайты с многоуровневой навигацией: состояние поиска, вкладок и сортировки сохраняется только внутри конкретного layout, обеспечивая плавный UX без лишних перерендеров.

Хотите, я могу сделать подробную визуальную схему с примером изоляции состояния layout с несколькими маршрутами, чтобы было сразу понятно, где и как хранится состояние?