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

Janus picture Janus · Oct 17, 2019 · Viewed 10.2k times · Source

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:

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?

Answer

Andrii Golubenko picture Andrii Golubenko · Oct 17, 2019

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.