Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 676
Janus Avatar asked Oct 17 '19 14:10

Janus


People also ask

How do you resolve invalid hook call hooks can only be called inside of the body of a function component?

If the error persists, try to delete your node_modules and package-lock. json (not package. json ) files, re-run npm install and restart your IDE. The error is often caused by having multiple versions of react in the same project.

How do you fix an invalid hook call?

This problem can also come up when you use npm link or an equivalent. In that case, your bundler might “see” two Reacts — one in application folder and one in your library folder. Assuming myapp and mylib are sibling folders, one possible fix is to run npm link ../myapp/node_modules/react from mylib .

Can I call hooks outside component?

You can not use hooks outside a component function, it is simply how they work. But, you can make a composition of hooks. React relies on an amount and order of how hooks appear in the component function.

Can you use React hooks outside component?

"Hooks can only be called inside the body of a function component" ReactJS Error. "Hooks can only be called inside the body of a function component." This is a common warning that developers run into when starting out with hooks in React.


2 Answers

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.

like image 66
Andrii Golubenko Avatar answered Sep 19 '22 16:09

Andrii Golubenko


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"
}

like image 35
Daniel Santana Avatar answered Sep 20 '22 16:09

Daniel Santana