react-router-dom: Invalid hook call, Hooks can only be called inside of the body of a function component

20,856

Solution 1

You can't use hooks inside Catalog component because it is a class component. So you have two ways to resolve your issue:

  1. Rewrite your component from class to functional.
  2. Do not use useRouteMatch inside Catalog component. If you need to get match data inside a component, you need to use withRouter high-order component.

So if you select second way, you will need to wrap your Catalog component in withRouter:

export default withRouter(Catalog);

Change one row in render function from:

let { path, url } = useRouteMatch();

To:

const { path, url } = this.props.match;

And do not forget to change the import of your Catalog component, because now your component exports as default.

Solution 2

As I had the same issue when setting up my React Router with Typescript, I will detail a little bit more Andrii answer in 4 steps:

1 - npm/yarn packages

yarn add react-router-dom --save
yarn add @types/react-router-dom --save-dev

or

npm install react-router-dom --save
npm install @types/react-router-dom --save-dev

2 - index.tsx


1) When importing your higher order component (App in the present case), do not use curly brackets as App will be exported as default;

2) BrowserRouter needs to be in a upper level rather the class that will be exported as "default withRouter(Class)", in order to prevent the following error:

"You should not use Route or withRouter() outside a Router"

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import * as serviceWorker from './serviceWorker';
import App from './app';

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);

serviceWorker.unregister();

3 - app.tsx


1) Import from react-router-dom, withRouter & RouteComponentProps (or your own PropType definition);

2) Extend React.Component and use the RouteComponentProps interface;

3) Pass the props to components you want to share routing data;

4) Export the higher order class as default withRouter.

import React, { ReactElement } from 'react';
import { Switch, Route, withRouter, RouteComponentProps } from 'react-router-dom';
import { ExpensesPage } from './pages/expenses/expenses.page';
import { HomePage } from './pages/home/home.page';
import { HeaderComponent } from './components/header/header.component';
import './app.scss';

class App extends React.Component<RouteComponentProps> {
  public render(): ReactElement {
    return (
      <div className='playground'>
        <HeaderComponent {...this.props} />
        <div className="playground-content">
          <Switch>
            <Route exact path='/' component={HomePage} {...this.props} />
            <Route exact path='/expenses' component={ExpensesPage} {...this.props} />
          </Switch>
        </div>
      </div>
    );
  }
}

export default withRouter(App);

4 - header.component


Through your RouteComponentProps extending your class, you can access normally the routing props as history, location and match as bellow:

import React, { ReactElement } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import './header.component.scss';

export class HeaderComponent extends React.Component<RouteComponentProps> {
  public render(): ReactElement {
    const { location } = this.props;
    console.log(location.pathname);

    return (
      <header className="header">
        {/* ... */}
      </header >
    );
  }
}

Hope it helps because I had a bit of challenge to make this works in a simple environment with webpack and no redux. Last time working properly with the following versions:

{
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-router-dom": "^5.1.2",
    "sass-loader": "^8.0.2",
    "style-loader": "^1.1.3",
    "typescript": "^3.8.2",
    "webpack": "^4.41.6",
    "webpack-dev-server": "^3.10.3",
},
{
    "@types/react-router-dom": "^5.1.3",
    "webpack-cli": "^3.3.11"
}

Share:
20,856
Janus
Author by

Janus

Updated on January 12, 2022

Comments

  • Janus
    Janus over 2 years

    I try to nest a route: I have a catalog of products in a Catalog component, which matches with url "backoffice/catalog".

    I want to route to Edition component if the url matches with "backoffice/catalog/edit", but I need the Edition component to be a child of Catalog to share props.

    I really don't understand why the nested route doesn't work, please save me ! And don't hesitate to tell me if anything is wrong with my App, I know JavaScript well, but I'm starting with React.

    Here is my App component:

    import React from "react";
    import { Route, Switch } from "react-router-dom";
    import { Home } from "./components/Static/Home.js";
    import { Dashboard } from "./components/Backoffice/Dashboard.js";
    import { Catalog } from "./components/Backoffice/catalog/Catalog.js";
    import { Login } from "./components/Login/Login.js";
    import { Signup } from "./components/Signup/Signup.js";
    import { PrivateRoute } from "./components/PrivateRoute.js";
    import "./scss/App.scss";
    import {Header} from "./components/Structure/Header";
    import {BOHeader} from "./components/Structure/Backoffice/Header";
    import {List} from "./components/Listing/List";
    
    function App()
    {
      return (
        <div className="App">
          <div className="App-content">
              <Switch>
                  <Route path='/backoffice' component={BOHeader} />
                  <Route path='/' component={Header} />
              </Switch>
              <Switch>
                  <Route exact path='/' component={Home} />
                  <Route exact path='/login' component={Login} />
                  <Route exact path='/signup' component={Signup} />
                  <Route path='/listing' component={List}/>
                  <PrivateRoute exact path='/backoffice' component={Dashboard}/>
                  <PrivateRoute exact path='/backoffice/catalog' component={Catalog}/>
              </Switch>
          </div>
        </div>
      );
    }
    
    export default App;

    Here is my Catalog component (the route is made in the render method:

    import React from 'react';
    import Data from '../../../Utils/Data';
    import {Product} from './Product';
    import {Edition} from './Edition';
    import {
        BrowserRouter as Router,
        Switch,
        Route,
        Link,
        useRouteMatch,
        useParams
    } from "react-router-dom";
    
    export class Catalog extends React.Component
    {
        state = {
            title: '',
            products: [],
            editionProduct: null
        };
    
        obtainProducts = () =>
        {
            Data.products.obtain()
                .then(products => {this.setState({products: products});})
        };
    
        editProductHandler = product =>
        {
            this.setState({editionProduct: product});
        };
    
        saveProductHandler = product =>
        {
            Data.products.save(product).then(() => {
                this.state.products.map(item => {
                    item = item._id === product._id ? product : item;
                    return item;
                })
            });
        };
    
        deleteProductHandler = event =>
        {
            const productId = event.target.closest('.product-actions').dataset.productid;
            let products = this.state.products.filter(product => {
                return product._id !== productId;
            });
            this.setState({products: products}, () => {
                Data.products.remove(productId);
            });
        };
    
        displayProducts = () =>
        {
            return this.state.products.map(product => {
               return (
                    <li key={product._id} className='catalog-item'>
                       <Product
                           deleteProductHandler={this.deleteProductHandler}
                           editProductHandler={this.editProductHandler}
                           data={product}
                       />
                   </li>
                )
            });
        };
    
    
        componentWillMount()
        {
            this.obtainProducts();
        }
    
        render() {
            const Products = this.displayProducts();
            let { path, url } = useRouteMatch();
            return (
                <div className={this.state.editionProduct ? 'catalog edit' : 'catalog'}>
                    <h1>Catalog</h1>
                    <Switch>
                        <Route exact path={path}>
                            <ul className='catalog-list'>{Products}</ul>
                        </Route>
                        <Route path={`${path}/edit`}>
                            <Edition saveProductHandler={this.saveProductHandler} product={this.state.editionProduct} />
                        </Route>
                    </Switch>
                </div>
            );
        }
    }

    Any ideas?