Search code examples
javascriptreactjstypescriptreact-routerreact-router-dom

Cannot use "useLocation" outside of react-router <RouterProvider> Uncaught Error: useLocation() may be used only in the context of a <Router>


I'm trying to use the useLocation hook in my component:

import React, { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { connect } from 'react-redux';
import { useNProgress } from '@tanem/react-nprogress';
import type { PayloadAction } from '@reduxjs/toolkit';

import type { AppState } from '@/store/app';
import { uiActions } from '@/store/reducers/ui';

import EDProgressBarView from './EDProgressBar.view';

interface IPropsFromState {
    readonly isProgressBarVisible: boolean;
}

interface IPropsFromDispatch {
    readonly closeProgressBar: () => PayloadAction;
}

interface IProps extends IPropsFromState, IPropsFromDispatch {}

const EDProgressBar: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => {
    const { animationDuration, isFinished, progress } = useNProgress({
        isAnimating: props.isProgressBarVisible,
        incrementDuration: 200,
    });

    const location = useLocation();

    useEffect(() => {
        props.closeProgressBar();
    }, [location]);

    return (
        <EDProgressBarView
            animationDuration={animationDuration}
            isFinished={isFinished}
            progress={progress}
        />
    );
};

EDProgressBar.displayName = 'EDProgressBar';
EDProgressBar.defaultProps = {};

const mapStateToProps = (state: AppState) => {
    return {
        isProgressBarVisible: state.ui.isProgressBarVisible,
    };
};

export default connect(mapStateToProps, {
    closeProgressBar: uiActions.closeProgressBar,
})(React.memo(EDProgressBar));

And I place this component here:

import React, { Suspense, useMemo } from 'react';
import { RouterProvider, createBrowserRouter } from 'react-router-dom';

import EDNotification from '@/ui/EDNotification';
import EDProgressBar from '@/ui/EDProgressBar';

import RouterBuilder from './App.router';

interface IProps {
    readonly isAuthenticated: boolean | null;
}

const AppView: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => {
    const routes = useMemo(() => {
        return RouterBuilder(props.isAuthenticated);
    }, [props.isAuthenticated]);

    return (
        <Suspense fallback={null}>
            <RouterProvider router={createBrowserRouter(routes)} fallbackElement={null} />

            <div id="backdrop-root" />
            <div id="overlay-root" />

            <EDNotification />
            <EDProgressBar />
        </Suspense>
    );
};

AppView.displayName = 'AppView';
AppView.defaultProps = {};

export default React.memo(AppView);

So then I get an error:

Uncaught Error: useLocation() may be used only in the context of a <Router> component.

But I don't understand how can I do such thing if RouterProvider component does not accept any children of something like a component to inject it. I need this EDProgressBar component to exist in the application regardless of the active route. I need it to exist in every page.

How can I do so?

This is my RouterBuild:

import React from 'react';
import { type RouteObject } from 'react-router-dom';


const CliAuth = React.lazy(() => import('./pages/CliAuth'));
const CliAuthenticated = React.lazy(() => import('./pages/CliAuthenticated'));
const NotFound = React.lazy(() => import('./pages/NotFound'));


const RouterBuilder = (isAuthenticated: boolean | null) => {

    const generalRoutes: RouteObject[] = [
        {
            path: 'cli-auth',
            element: <CliAuth />,
        },
        {
            path: 'cli-authenticated',
            element: <CliAuthenticated />,
        },
        {
            path: 'not-found',
            element: <NotFound />,
        },
        {
            path: '*',
            element: isAuthenticated === null ? null : <NotFound />,
        },
    ];

    return [...(isAuthenticated ? authorizedRoutes : unAuthorizedRoutes), ...generalRoutes];
};

export default RouterBuilder;

Note that I removed some routes because they are irrelevant here, but the important thing is that I use the loader key in some, so I need this builder.


Solution

  • The useLocation and other RRD hooks can't be used outside any routing context provided by a router. I suggest creating a layout route that renders the EDNotification and EDProgressBar components and the backdrop and overlay divs. This layout route will be included in the routes returned by RouterBuilder. This allows the components accessing the routing context to have the router higher than them in the ReactTree.

    Example:

    import { Outlet } from 'react-router-dom';
    
    const AppLayout = () => (
      <>
        <Outlet />
        <div id="backdrop-root" />
        <div id="overlay-root" />
    
        <EDNotification />
        <EDProgressBar />
      </>
    );
    
    const RouterBuilder = (isAuthenticated: boolean | null) => {
      const generalRoutes: RouteObject[] = [
        {
          path: 'cli-auth',
          element: <CliAuth />,
        },
        {
          path: 'cli-authenticated',
          element: <CliAuthenticated />,
        },
        {
          path: 'not-found',
          element: <NotFound />,
        },
        {
          path: '*',
          element: isAuthenticated === null ? null : <NotFound />,
        },
      ];
    
      const routes = [
        {
          element: <AppLayout />
          children: [
            ...(isAuthenticated ? authorizedRoutes : unAuthorizedRoutes),
            ...generalRoutes
          ],
        }
      ];
    
      return routes;
    };
    
    const AppView: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => {
      const routes = useMemo(() => {
        return RouterBuilder(props.isAuthenticated);
      }, [props.isAuthenticated]);
    
      return (
        <Suspense fallback={null}>
          <RouterProvider
            router={createBrowserRouter(routes)}
            fallbackElement={null}
          />
        </Suspense>
      );
    };