Search code examples
reactjsreact-router-domupgradereact-ref

Upgrading React-router-dom v6 multiple errors


So I've been upgrading modules for a project that is not mine so I'm not familiar with the code, when I tried upgrading react-router-dom from 5.2.0 ❯ 6.3.0 I stumbled with the way the routes are implemented. Having replaced the reprecated modules I get multiple errors that basically say the routes are not being defined correctly but I tried everything and I can't get it to work. These are the errors I get when I load the page.

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Matched leaf route at location "/" does not have an element. This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.

Uncaught Error: [Navigate] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>

This is the app component that deals with the routes.

import routes from './App.routes'
import { lazy } from 'react'
import AppContainer from './containers/app'
import AuthContainer from './containers/auth'
import { Route, Routes, Navigate, useNavigate, useRoutes } from 'react-router-dom'
import { ModalError } from './components/ModalError/ModalError'
import { EnvironmentProvider } from './contexts/Environment'
import { useDispatch, useSelector } from 'react-redux'
import { hiddenError } from './store/slices/health'
import { FullScreenLoading } from './components/FullScreenLoading/FullScreenLoading'
import { Suspense, useEffect, useState } from 'react'
import * as actionsProfile from './store/slices/profile'
import { firebaseInit, getValuesRemoteConfig } from './service/firebase'
import { setSecret } from './service/cryptojs'
import axios from './config/axios'
import Maintenance from './pages/common/maintenance/maintenance'
import { googleAnalyticsRegisterView } from './components/GoogleAnalytics/GoogleAnalytics'

const App = () => {
  const [isLoadingConfig, setIsLoadingConfig] = useState(true)

  const [configBeSetted, setConfigBeSetted] = useState(false)

  const [appInitialized, setAppInitialized] = useState(false)

  const [configuration, setConfiguration] = useState({
    isMaintenanceWebMessage: null,
    isMaintenanceWeb: null,
    apiUrlRemote: null,
    timeoutRemote: null,
    secretRemote: null,
    publicWebUrl: null,
    publicTurnsUrl: null,
    successRedirectUri: null,
    errorRedirectUri: null,
    googlePlayUrl: null,
    appStoreUrl: null,
  })

  const token = JSON.parse(localStorage.getItem('tsu'))?.token

  const { showError } = useSelector((state) => state.health.health)

  const dispatch = useDispatch()

  useEffect(() => {
    firebaseInit(async (app) => {
      const config = await getValuesRemoteConfig(app)

      //Production
      axios.defaults.baseURL = config.apiUrlRemote
      axios.defaults.timeout = config.timeoutRemote
      setSecret(config.secretRemote)

      setConfiguration(config)

      setIsLoadingConfig(false)

      setConfigBeSetted(true)

      setAppInitialized(true)

      token && dispatch(actionsProfile.getProfileData())
    })
  }, [token])

  if (isLoadingConfig || !configBeSetted) return <FullScreenLoading />

  if (configBeSetted && configuration?.isMaintenanceWeb) {
    return (
      <EnvironmentProvider config={configuration}>
        <Maintenance message={configuration?.isMaintenanceWebMessage} />
      </EnvironmentProvider>
    )
  }
  return (
    <Suspense fallback={<FullScreenLoading />}>
      <EnvironmentProvider config={configuration}>
        {showError && <ModalError onClose={() => dispatch(hiddenError())} />}
        <Routes>
          {routes
            .map((route) => ({
              ...route,
              component: appInitialized
                ? googleAnalyticsRegisterView(route.component)
                : route.component,
            }))
            .map((route, i) => {
              const Container = route.private ? AppContainer : AuthContainer
              return (
                <Route
                  key={i}
                  path={route.path}
                  exact={route.exact}
                  render={(props) => {
                    return (
                      <Container key={i}>
                        <route.component {...props} />
                      </Container>
                    )
                  }}
                />
              )
            })}
          <Navigate to={token ? '/home' : '/'} />
        </Routes>
      </EnvironmentProvider>
    </Suspense>
  )
}

export default App

This is the routes component.

import { lazy } from 'react'
import { googleAnalyticsRegisterView } from './components/GoogleAnalytics/GoogleAnalytics'

const Home = lazy(() => import('./pages/app/home'))
const MyData = lazy(() => import('./pages/app/my-data'))
const ClientAttention = lazy(() => import('./pages/app/client-atention'))
const PhoneAssistance = lazy(() => import('./pages/app/phone-assitance'))
const InsuranceDetails = lazy(() => import('./pages/app/insurance-details'))
const FeePayment = lazy(() => import('./pages/app/fee-payment'))
const MyInsurance = lazy(() => import('./pages/app/my-insurance'))
const ReportClaim = lazy(() => import('./pages/app/report-claim'))
const Notifications = lazy(() => import('./pages/app/notifications'))
const Login = lazy(() => import('./pages/auth/login'))
const RecoverPassword = lazy(() => import('./pages/auth/recover-password'))
const Register = lazy(() => import('./pages/auth/register'))
const NewPassword = lazy(() => import('./pages/auth/new-password'))
const FrecuentlyQuestions = lazy(() =>
  import('./pages/app/frecuently-question')
)
const ProofOfPayment = lazy(() =>
  import('./pages/common/proof-of-payment/ProofOfPayment')
)
const SuccessPayment = lazy(() =>
  import('./pages/common/payments/success/SuccessPayment')
)
const ErrorPayment = lazy(() =>
  import('./pages/common/payments/error/ErrorPayment')
)
const AppRedirect = lazy(() => import('./pages/auth/app-redirect'))

export const privateRoutes = [
  '/home',
  '/my-data',
  '/phone-assistance',
  '/client-attention',
  '/details',
  '/fee-payment',
  '/my-insurances',
  '/report-claim',
  '/notifications',
  '/faqs',
]

export const publicRoutes = [
  '/',
  '/recover-password',
  '/new-password',
  '/register',
  '/certifacates',
  '/app-redirect',
]

const routes = [
  {
    component: SuccessPayment,
    path: '/success-payment',
    exact: true,
    private: !!localStorage.getItem('tsu'),
  },
  {
    component: ErrorPayment,
    path: '/error-payment',
    exact: true,
    private: !!localStorage.getItem('tsu'),
  },
  {
    component: Login,
    path: '/',
    exact: true,
    private: false,
  },
  {
    component: RecoverPassword,
    path: '/recover-password',
    exact: true,
    private: false,
  },
  {
    component: NewPassword,
    path: '/new-password',
    exact: true,
    private: false,
  },
  {
    component: Register,
    path: '/register',
    exact: true,
    private: false,
  },
  {
    component: Home,
    path: '/home',
    exact: true,
    private: true,
  },
  {
    component: MyData,
    path: '/my-data',
    exact: true,
    private: true,
  },
  {
    component: PhoneAssistance,
    path: '/phone-assistance',
    exact: true,
    private: true,
  },
  {
    component: ClientAttention,
    path: '/client-attention',
    exact: true,
    private: true,
  }
]

export default routes

And this is the main component where app is rendered

import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import GlobalStyles from './global-styles'
import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary'
import store, { history } from './store'
import { HashRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import * as Sentry from '@sentry/react'
import { BrowserTracing } from '@sentry/tracing'

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_KEY,
  integrations: [new BrowserTracing()],
  tracesSampleRate: 1.0,
})

const container = document.getElementById('root')
const root = createRoot(container) // createRoot(container!) if you use TypeScript
root.render(
  <React.StrictMode>
    <ErrorBoundary>
      <Provider store={store}>
        <HashRouter ref={history}>
          <GlobalStyles />
          <App />
        </HashRouter>
      </Provider>
    </ErrorBoundary>
  </React.StrictMode>
)

History is exported like export const history = createRef();

I know there is something about that ref property or HashRouter but in the documentation I can't find it.


Solution

  • Issue 1

    Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

    This does indeed appear to be related to the HashRouter component. You can look at the type declaration and see that the HashRouter doesn't consume a ref prop.

    declare function HashRouter(
      props: HashRouterProps
    ): React.ReactElement;
    
    interface HashRouterProps {
      basename?: string;
      children?: React.ReactNode;
      window?: Window;
    }
    

    And also inspect the source code to verify it also doesn't forward any React ref.

    /**
     * A `<Router>` for use in web browsers. Stores the location in the hash
     * portion of the URL so it is not sent to the server.
     */
    export function HashRouter({ basename, children, window }: HashRouterProps) {
      let historyRef = React.useRef<HashHistory>();
      if (historyRef.current == null) {
        historyRef.current = createHashHistory({ window, v5Compat: true });
      }
    
      let history = historyRef.current;
      let [state, setState] = React.useState({
        action: history.action,
        location: history.location,
      });
    
      React.useLayoutEffect(() => history.listen(setState), [history]);
    
      return (
        <Router
          basename={basename}
          children={children}
          location={state.location}
          navigationType={state.action}
          navigator={history}
        />
      );
    }
    

    The Hashrouter instantiates and maintains it's own history reference internally. To fix the ref issue simply remove the ref.

    const container = document.getElementById('root');
    const root = createRoot(container);
    
    root.render(
      <React.StrictMode>
        <ErrorBoundary>
          <Provider store={store}>
            <HashRouter>
              <GlobalStyles />
              <App />
            </HashRouter>
          </Provider>
        </ErrorBoundary>
      </React.StrictMode>
    );
    

    If you have need to use a custom hashHistory object then use the HistoryRouter and follow the instructions for rendering. Be sure to have history@5 installed as a project dependency.

    Issue 2

    Matched leaf route at location "/" does not have an element. This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.

    The Route component API changed significantly from v5 to v6. The Route components render all their content on a single element prop taking a React.ReactNode, a.k.a. JSX. exact is no longer used now that all routes are always exactly matched.

    {routes
      .map((route, i) => {
        const Container = route.private ? AppContainer : AuthContainer;
        const Component = appInitialized
          ? googleAnalyticsRegisterView(route.component)
          : route.component;
        return (
          <Route
            key={i}
            path={route.path}
            element={(
              <Container key={i}>
                <Component />
              </Container>
            )}
          />
        )
      })
    }
    

    This also means there are no longer any route props. The routed components need to use the React hooks:

    • useNavigate to access the navigate function that replaced history
    • useParams to access path params that replaced match.params
    • useLocation to access the location object

    Issue 3

    Uncaught Error: [Navigate] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>

    Only Route or React.Fragment are valid children of the Routes component. If you want to redirect that Navigate component still needs to be rendered on a route. Specify the replace prop so the navigation action is a REPLACE instead of a PUSH.

    <Route
      path="*"
      element={<Navigate to={token ? '/home' : '/'} replace />}
    />