react-router-dom: Invalid hook call, Hooks can only be called inside of the body of a function component
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:
- Rewrite your component from class to functional.
- Do not use
useRouteMatch
insideCatalog
component. If you need to getmatch
data inside a component, you need to usewithRouter
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"
}
Janus
Updated on January 12, 2022Comments
-
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?