import * as React from 'react'; import { buildSearch, IRoute, parseRoute } from './routes'; interface INavigateOptions { /** Заменить текущую запись истории (replaceState), а не добавить новую. */ replace?: boolean; } interface IRouterValue { route: IRoute; /** Перейти в раздел: меняет адрес (history.pushState) и перерисовывает приложение. */ navigate: (route: IRoute, options?: INavigateOptions) => void; /** Полный адрес раздела — для настоящих ссылок (Ctrl+клик, «открыть в новой вкладке»). */ buildHref: (route: IRoute) => string; } const RouterContext = React.createContext(undefined); function readCurrentRoute(): IRoute { return parseRoute(window.location.search); } function buildHref(route: IRoute): string { return `${window.location.pathname}${buildSearch(route, window.location.search)}`; } export const RouterProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [route, setRoute] = React.useState(readCurrentRoute); // Кнопки «назад/вперёд» браузера меняют адрес без нашего участия — перечитываем маршрут. React.useEffect(() => { const onPopState = (): void => setRoute(readCurrentRoute()); window.addEventListener('popstate', onPopState); return () => window.removeEventListener('popstate', onPopState); }, []); const navigate = React.useCallback((next: IRoute, options: INavigateOptions = {}): void => { if (options.replace) { window.history.replaceState(undefined, '', buildHref(next)); } else { window.history.pushState(undefined, '', buildHref(next)); } setRoute(next); window.scrollTo(0, 0); }, []); const value = React.useMemo(() => ({ route, navigate, buildHref }), [route, navigate]); return {children}; }; export function useRouter(): IRouterValue { const router = React.useContext(RouterContext); if (!router) { throw new Error('useRouter: компонент отрисован вне RouterProvider'); } return router; }