← React/React Router: навигация#271 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksТема: React: хуки и экосистема

React Router: навигация

Клиентская маршрутизация

React — SPA (Single Page Application). Браузер загружает страницу один раз, а затем React-приложение само управляет тем, что отображается, в зависимости от URL. Перехода между страницами с перезагрузкой нет — это быстрее и обеспечивает плавный пользовательский опыт.

React Router — наиболее популярная библиотека маршрутизации для React. Актуальная версия: React Router v6.

Базовая структура React Router v6

import { BrowserRouter, Routes, Route, Link, NavLink } from 'react-router-dom'

function App() {
  return (
    <BrowserRouter>
      {/* Навигация */}
      <nav>
        <Link to="/">Главная</Link>
        {/* NavLink добавляет класс "active" к активной ссылке */}
        <NavLink to="/about" className={({ isActive }) => isActive ? 'active' : ''}>
          О нас
        </NavLink>
        <Link to="/users">Пользователи</Link>
      </nav>

      {/* Маршруты — отображается только совпадающий */}
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
        <Route path="/users" element={<UsersPage />} />
        <Route path="/users/:id" element={<UserPage />} />
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </BrowserRouter>
  )
}

useParams: параметры URL

// URL: /users/42
function UserPage() {
  const { id } = useParams()  // { id: "42" } — всегда строка!

  const userId = parseInt(id, 10)  // конвертируем в число если нужно
  const { data: user } = useFetch(`/api/users/${userId}`)

  return <div>{user?.name}</div>
}

useNavigate: программная навигация

function LoginForm() {
  const navigate = useNavigate()

  const handleSubmit = async (e) => {
    e.preventDefault()
    await login(credentials)
    navigate('/dashboard')          // переход вперёд
    navigate(-1)                    // назад (как history.back())
    navigate('/home', { replace: true })  // без добавления в историю
  }

  return <form onSubmit={handleSubmit}>...</form>
}

useSearchParams: параметры запроса

// URL: /search?q=react&page=2
function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams()

  const query = searchParams.get('q') ?? ''
  const page = parseInt(searchParams.get('page') ?? '1', 10)

  const handleSearch = (q) => {
    setSearchParams({ q, page: '1' })  // обновляет URL без перезагрузки
  }

  return <input value={query} onChange={e => handleSearch(e.target.value)} />
}

Вложенные маршруты

function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={<DashboardLayout />}>
        {/* Дочерние маршруты рендерятся через <Outlet /> */}
        <Route index element={<DashboardHome />} />         {/* /dashboard */}
        <Route path="analytics" element={<Analytics />} />  {/* /dashboard/analytics */}
        <Route path="settings" element={<Settings />} />    {/* /dashboard/settings */}
      </Route>
    </Routes>
  )
}

function DashboardLayout() {
  return (
    <div>
      <Sidebar />
      <main>
        <Outlet />  {/* Здесь рендерится активный дочерний маршрут */}
      </main>
    </div>
  )
}

Защищённые маршруты

function PrivateRoute({ children }) {
  const { isAuthenticated } = useAuth()

  if (!isAuthenticated) {
    return <Navigate to="/login" replace />
  }

  return children
}

// Использование
<Route
  path="/admin"
  element={
    <PrivateRoute>
      <AdminPage />
    </PrivateRoute>
  }
/>

Примеры

Реализация hash-роутера с нуля: window.hashchange, рендер по текущему hash, навигация и параметры

// Реализуем простой роутер на основе window.location.hash.
// Это покажет, как клиентская маршрутизация работает без перезагрузки страницы.

// --- Создаём роутер ---

function createHashRouter(routes) {
  // routes: { '/': handler, '/about': handler, '/users/:id': handler }
  const listeners = new Set()

  // Парсим шаблон маршрута: '/users/:id' -> регулярное выражение
  function parseRoute(pattern) {
    const params = []
    const regexStr = pattern.replace(/:([^/]+)/g, (_, name) => {
      params.push(name)
      return '([^/]+)'
    })
    return { regex: new RegExp(`^${regexStr}$`), params }
  }

  // Находим подходящий маршрут и извлекаем параметры
  function matchRoute(path) {
    for (const [pattern, handler] of Object.entries(routes)) {
      if (pattern === '*') continue  // 404 обрабатываем последним

      const { regex, params } = parseRoute(pattern)
      const match = path.match(regex)

      if (match) {
        const paramValues = {}
        params.forEach((name, i) => {
          paramValues[name] = match[i + 1]
        })
        return { handler, params: paramValues }
      }
    }

    // 404: если есть маршрут '*'
    if (routes['*']) {
      return { handler: routes['*'], params: {} }
    }

    return null
  }

  // Текущий path из hash
  const getPath = () => {
    const hash = window.location.hash.slice(1)  // убираем '#'
    return hash || '/'
  }

  // Рендер текущего маршрута
  function render() {
    const path = getPath()
    const match = matchRoute(path)

    console.log(`[Router] Навигация: ${path}`)

    if (match) {
      const result = match.handler({ params: match.params, path })
      listeners.forEach(fn => fn({ path, params: match.params, view: result }))
    }
  }

  // Слушаем изменения hash
  window.addEventListener('hashchange', render)

  return {
    // Программная навигация
    navigate(path) {
      window.location.hash = path
      // hashchange сработает автоматически
    },

    // Обратная навигация
    back() {
      window.history.back()
    },

    // Инициализация — рендерим текущий маршрут
    start() {
      render()
      return this
    },

    // Подписка на навигацию (как useNavigate)
    onNavigate(fn) {
      listeners.add(fn)
      return () => listeners.delete(fn)
    },

    destroy() {
      window.removeEventListener('hashchange', render)
    }
  }
}

// --- Описываем маршруты ---

const router = createHashRouter({
  '/': ({ path }) => {
    console.log('  Рендер: Главная страница')
    return 'Главная'
  },
  '/about': ({ path }) => {
    console.log('  Рендер: О нас')
    return 'О нас'
  },
  '/users': ({ path }) => {
    console.log('  Рендер: Список пользователей')
    return 'Пользователи'
  },
  '/users/:id': ({ params }) => {
    console.log(`  Рендер: Профиль пользователя #${params.id}`)
    return `Пользователь ${params.id}`
  },
  '*': ({ path }) => {
    console.log(`  Рендер: 404 — страница "${path}" не найдена`)
    return '404'
  }
})

// Подписываемся на навигацию
router.onNavigate(({ path, params, view }) => {
  console.log('  -> Текущий вид: "' + view + '", params:', params)
})

// --- Симуляция навигации ---

console.log('=== Запуск роутера ===')
// Эмулируем hashchange вручную (в браузере это делает window.location.hash = ...)
const simulateNav = (path) => {
  Object.defineProperty(window, 'location', {
    value: { hash: '#' + path, ...window.location },
    writable: true
  })
  window.dispatchEvent(new HashChangeEvent('hashchange'))
}

// В браузере просто:
// router.navigate('/')
// router.navigate('/about')
// router.navigate('/users/42')

console.log('
В реальном браузере:')
console.log('router.navigate("/")       -> рендер "Главная"')
console.log('router.navigate("/about")  -> рендер "О нас"')
console.log('router.navigate("/users/42") -> params: { id: "42" }')
console.log('router.navigate("/unknown") -> рендер 404')
console.log('
React Router делает то же самое, но через History API (без #)')

React Router: навигация

Клиентская маршрутизация

React — SPA (Single Page Application). Браузер загружает страницу один раз, а затем React-приложение само управляет тем, что отображается, в зависимости от URL. Перехода между страницами с перезагрузкой нет — это быстрее и обеспечивает плавный пользовательский опыт.

React Router — наиболее популярная библиотека маршрутизации для React. Актуальная версия: React Router v6.

Базовая структура React Router v6

import { BrowserRouter, Routes, Route, Link, NavLink } from 'react-router-dom'

function App() {
  return (
    <BrowserRouter>
      {/* Навигация */}
      <nav>
        <Link to="/">Главная</Link>
        {/* NavLink добавляет класс "active" к активной ссылке */}
        <NavLink to="/about" className={({ isActive }) => isActive ? 'active' : ''}>
          О нас
        </NavLink>
        <Link to="/users">Пользователи</Link>
      </nav>

      {/* Маршруты — отображается только совпадающий */}
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
        <Route path="/users" element={<UsersPage />} />
        <Route path="/users/:id" element={<UserPage />} />
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </BrowserRouter>
  )
}

useParams: параметры URL

// URL: /users/42
function UserPage() {
  const { id } = useParams()  // { id: "42" } — всегда строка!

  const userId = parseInt(id, 10)  // конвертируем в число если нужно
  const { data: user } = useFetch(`/api/users/${userId}`)

  return <div>{user?.name}</div>
}

useNavigate: программная навигация

function LoginForm() {
  const navigate = useNavigate()

  const handleSubmit = async (e) => {
    e.preventDefault()
    await login(credentials)
    navigate('/dashboard')          // переход вперёд
    navigate(-1)                    // назад (как history.back())
    navigate('/home', { replace: true })  // без добавления в историю
  }

  return <form onSubmit={handleSubmit}>...</form>
}

useSearchParams: параметры запроса

// URL: /search?q=react&page=2
function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams()

  const query = searchParams.get('q') ?? ''
  const page = parseInt(searchParams.get('page') ?? '1', 10)

  const handleSearch = (q) => {
    setSearchParams({ q, page: '1' })  // обновляет URL без перезагрузки
  }

  return <input value={query} onChange={e => handleSearch(e.target.value)} />
}

Вложенные маршруты

function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={<DashboardLayout />}>
        {/* Дочерние маршруты рендерятся через <Outlet /> */}
        <Route index element={<DashboardHome />} />         {/* /dashboard */}
        <Route path="analytics" element={<Analytics />} />  {/* /dashboard/analytics */}
        <Route path="settings" element={<Settings />} />    {/* /dashboard/settings */}
      </Route>
    </Routes>
  )
}

function DashboardLayout() {
  return (
    <div>
      <Sidebar />
      <main>
        <Outlet />  {/* Здесь рендерится активный дочерний маршрут */}
      </main>
    </div>
  )
}

Защищённые маршруты

function PrivateRoute({ children }) {
  const { isAuthenticated } = useAuth()

  if (!isAuthenticated) {
    return <Navigate to="/login" replace />
  }

  return children
}

// Использование
<Route
  path="/admin"
  element={
    <PrivateRoute>
      <AdminPage />
    </PrivateRoute>
  }
/>

Примеры

Реализация hash-роутера с нуля: window.hashchange, рендер по текущему hash, навигация и параметры

// Реализуем простой роутер на основе window.location.hash.
// Это покажет, как клиентская маршрутизация работает без перезагрузки страницы.

// --- Создаём роутер ---

function createHashRouter(routes) {
  // routes: { '/': handler, '/about': handler, '/users/:id': handler }
  const listeners = new Set()

  // Парсим шаблон маршрута: '/users/:id' -> регулярное выражение
  function parseRoute(pattern) {
    const params = []
    const regexStr = pattern.replace(/:([^/]+)/g, (_, name) => {
      params.push(name)
      return '([^/]+)'
    })
    return { regex: new RegExp(`^${regexStr}$`), params }
  }

  // Находим подходящий маршрут и извлекаем параметры
  function matchRoute(path) {
    for (const [pattern, handler] of Object.entries(routes)) {
      if (pattern === '*') continue  // 404 обрабатываем последним

      const { regex, params } = parseRoute(pattern)
      const match = path.match(regex)

      if (match) {
        const paramValues = {}
        params.forEach((name, i) => {
          paramValues[name] = match[i + 1]
        })
        return { handler, params: paramValues }
      }
    }

    // 404: если есть маршрут '*'
    if (routes['*']) {
      return { handler: routes['*'], params: {} }
    }

    return null
  }

  // Текущий path из hash
  const getPath = () => {
    const hash = window.location.hash.slice(1)  // убираем '#'
    return hash || '/'
  }

  // Рендер текущего маршрута
  function render() {
    const path = getPath()
    const match = matchRoute(path)

    console.log(`[Router] Навигация: ${path}`)

    if (match) {
      const result = match.handler({ params: match.params, path })
      listeners.forEach(fn => fn({ path, params: match.params, view: result }))
    }
  }

  // Слушаем изменения hash
  window.addEventListener('hashchange', render)

  return {
    // Программная навигация
    navigate(path) {
      window.location.hash = path
      // hashchange сработает автоматически
    },

    // Обратная навигация
    back() {
      window.history.back()
    },

    // Инициализация — рендерим текущий маршрут
    start() {
      render()
      return this
    },

    // Подписка на навигацию (как useNavigate)
    onNavigate(fn) {
      listeners.add(fn)
      return () => listeners.delete(fn)
    },

    destroy() {
      window.removeEventListener('hashchange', render)
    }
  }
}

// --- Описываем маршруты ---

const router = createHashRouter({
  '/': ({ path }) => {
    console.log('  Рендер: Главная страница')
    return 'Главная'
  },
  '/about': ({ path }) => {
    console.log('  Рендер: О нас')
    return 'О нас'
  },
  '/users': ({ path }) => {
    console.log('  Рендер: Список пользователей')
    return 'Пользователи'
  },
  '/users/:id': ({ params }) => {
    console.log(`  Рендер: Профиль пользователя #${params.id}`)
    return `Пользователь ${params.id}`
  },
  '*': ({ path }) => {
    console.log(`  Рендер: 404 — страница "${path}" не найдена`)
    return '404'
  }
})

// Подписываемся на навигацию
router.onNavigate(({ path, params, view }) => {
  console.log('  -> Текущий вид: "' + view + '", params:', params)
})

// --- Симуляция навигации ---

console.log('=== Запуск роутера ===')
// Эмулируем hashchange вручную (в браузере это делает window.location.hash = ...)
const simulateNav = (path) => {
  Object.defineProperty(window, 'location', {
    value: { hash: '#' + path, ...window.location },
    writable: true
  })
  window.dispatchEvent(new HashChangeEvent('hashchange'))
}

// В браузере просто:
// router.navigate('/')
// router.navigate('/about')
// router.navigate('/users/42')

console.log('
В реальном браузере:')
console.log('router.navigate("/")       -> рендер "Главная"')
console.log('router.navigate("/about")  -> рендер "О нас"')
console.log('router.navigate("/users/42") -> params: { id: "42" }')
console.log('router.navigate("/unknown") -> рендер 404')
console.log('
React Router делает то же самое, но через History API (без #)')

Задание

Создай мини-роутер на основе useState. Компонент App хранит currentPage в state. В зависимости от currentPage рендерится один из трёх "экранов": Home, About или Users. Навигация осуществляется через кнопки (не настоящие ссылки). Активная кнопка подсвечивается другим цветом.

Подсказка

useState("home") хранит текущую страницу. В switch(currentPage) возвращай нужный компонент: <HomePage />, <AboutPage />, <UsersPage />. В навигации: onClick={() => setCurrentPage(item.id)}. Активную кнопку выделяй: currentPage === item.id.

Загружаем среду выполнения...
Загружаем AI-помощника...