reactjstypescriptreact-router-domreact-class-based-component

React 18 Router 6 - "Invalid hook call" when using Outlet with class components


I have recently upgraded a React project to 18 and it's dependencies:

react-project package.json:

"dependencies": {
  "react": "^18.2.0",
  "react-dom": "^18.2.0",
  "react-router-dom": "^6.3.0",
  "react-scripts": "5.0.1"
},
"devDependencies": {
  "@types/node": "^16.11.45",
  "@types/react": "^18.0.15",
  "@types/react-dom": "^18.0.6",
  "@types/react-router-dom": "^5.3.3",
  "react-app-alias-ex": "^2.1.0",
  "react-app-rewired": "^2.2.1",
  "typescript": "^4.7.4"
},

I have a shared react base library, which I have upgraded as well and I use it on this project:

react-shared package.json:

"dependencies": {
  "dotenv": "^16.0.1",
  "graphql": "^16.5.0",
  "react": "^18.2.0",
  "react-dom": "^18.2.0",
  "react-router-dom": "^6.3.0",
  "redux-thunk": "^2.4.1",
  "typesafe-actions": "^5.1.0"
},
"devDependencies": {
  "@types/node": "^18.0.6",
  "@types/react": "^18.0.15",
  "@types/react-dom": "^18.0.6",
  "@types/react-router-dom": "^5.3.3",
  "tailwindcss": "^3.0.22",
  "tsconfig-paths": "^4.0.0",
  "typescript": "^4.7.4"
}

I am getting errors on the browser's console when I run it, saying:

Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: {...}
    useOutlet hooks.tsx:214
    Outlet components.tsx:110
    {...}

I have created a private and a public outlet, using react-router-dom:

react-shared private.outlet.tsx:

export default class PrivateOutlet extends Component {
  public constructor(props: any) {
    super(props);
  }

  public render(): RenderResult {
    if (loggedIn) {
      return <Outlet />;
    }
    return <Navigate to="/login" replace />;
  }
}

react-shared public.outlet.tsx:

export default class PublicOutlet extends Component {
  public constructor(props: any) {
    super(props);
  }

  public render(): RenderResult {
    if (!loggedIn) {
      return <Outlet />;
    }
    return <Navigate to="/" replace />;
  }
}

And the application:

react-project application.tsx:

export default class ApplicationComponent extends Component {
  public constructor(props: any) {
    super(props);
  }

  public render(): RenderResult {
    return (
      <BrowserRouter>
        <Routes>
          <Route path='/' element={<PrivateOutlet />}>
            <Route index element={<MainPage />} />
          </Route>
          <Route path='/login' element={<PublicOutlet />}>
            <Route index element={<LoginPage />} />
          </Route>
        </Routes>
      </BrowserRouter>
    );
  }
}

Does anyone have any idea how I can fix this problem and why is it happening? I'm trying to avoid changing my class components library to functional... Has react finally scr*wed me?

If anyone needs any more details I'll add them, just ask.

EDIT #1:

Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem. react.development.js:209
    React (4)
        printWarning
        error
        resolveDispatcher
        useContext
    useOutlet hooks.tsx:214
    Outlet components.tsx:110
    React (8)
        renderWithHooks
        mountIndeterminateComponent
        beginWork
        beginWork$1
        performUnitOfWork
        workLoopSync
        renderRootSync
        performConcurrentWorkOnRoot
    workLoop scheduler.development.js:266
    flushWork scheduler.development.js:239
    performWorkUntilDeadline scheduler.development.js:533
    (Async: EventHandlerNonNull)
    js scheduler.development.js:571
    js scheduler.development.js:633
    factory react refresh:6
    Webpack (24)
        __webpack_require__
        fn
        js
        factory
        __webpack_require__
        fn
        js
        js
        factory
        __webpack_require__
        fn
        js
        factory
        __webpack_require__
        fn
        js
        factory
        __webpack_require__
        fn
        tsx
        factory
        __webpack_require__
        <anonymous>
        <anonymous>

EDIT #2:

Uncaught TypeError: dispatcher is null
    useContext React
    useOutlet hooks.tsx:214
    Outlet components.tsx:110
    React (11)
        renderWithHooks
        mountIndeterminateComponent
        beginWork
        callCallback
        invokeGuardedCallbackDev
        invokeGuardedCallback
        beginWork$1
        performUnitOfWork
        workLoopSync
        renderRootSync
        performConcurrentWorkOnRoot
    workLoop scheduler.development.js:266
    flushWork scheduler.development.js:239
    performWorkUntilDeadline scheduler.development.js:533
    js scheduler.development.js:571
    js scheduler.development.js:633
    factory react refresh:6
    Webpack (24)
        __webpack_require__
        fn
        js
        factory
        __webpack_require__
        fn
        js
        js
        factory
        __webpack_require__
        fn
        js
        factory
        __webpack_require__
        fn
        js
        factory
        __webpack_require__
        fn
        tsx
        factory
        __webpack_require__
        <anonymous>
        <anonymous>

Solution

  • I've actually arrived at this solution through another question that spinned off from this one. Link here.

    The problem was duplicate references due to me using a shared project and the react-app-alias-ex package. I fixed it by adding the duplicate reference as an alias at config-overrides.js:

    const {aliasWebpack} = require('react-app-alias-ex');
    const path = require('path');
    
    const options = {
        alias: {
            '@my-shared': path.resolve('../PATH/TO/SHARED/src'),
            'react-router-dom': path.resolve('./node_modules/react-router-dom')
        }
    };
    
    module.exports = aliasWebpack(options);
    

    There was another very weird problem that was causing this change to not work and I was able to make work by following these steps rigorously:

    1. Delete /node_modules, /dist and /package-lock.json.
    2. Run npm install.
    3. Run npm run react-app-rewired build (If you are not using react-app-rewired, running npm run react-scripts build might work but I have not tested it).
    4. Run application and test.