This question already has answers here:
How to pass data from a page to another page using react router
(5 answers)
Closed 5 months ago.
for learning purposes I'm trying to create an eshop, but I'm facing issues when adding a product to the cart page. Product's 'id' is undefined in the cart. Products are fetched from my MongoDB database.
Routes component:
const PageRoutes = () => (
<Routes>
<Route path="/" element={<MainLayout />}>
<Route index element={<HomePage />} />
<Route path="/about-us" element={<AboutUs />} />
<Route path="/product-catalog" element={<ProductCatalog />} />
<Route path="/order" element={<OrderPage />} />
<Route path="/product/:id" element={<ProductInformation />} />
<Route path="auth/" element={<AuthLayout />}>
<Route path="login" element={<LoginPage />} />
<Route path="register" element={<RegisterPage />} />
</Route>
<Route path="/cart" element={<CartPage />} />
</Route>
</Routes>
);
Service file which fetches products:
const domain = 'http://localhost:8000';
const databaseCollection = 'api/products';
const relationsParams = 'joinBy=categoryId&joinBy=woodTypeId';
const fetchProducts = async () => {
const response = await fetch(`${domain}/${databaseCollection}/?${relationsParams}`);
const products = await response.json();
return products;
};
const fetchProductById = async (id) => {
const response = await fetch(`${domain}/${databaseCollection}/${id}?${relationsParams}`);
const product = await response.json();
console.log('fetchProductById', id); // it prints the id correctly, not as undefined
return product;
};
const ProductService = {
fetchProducts,
fetchProductById,
};
export default ProductService;
Cart component where I face the issue:
import * as React from 'react';
import { Container } from '#mui/material';
import { useParams } from 'react-router-dom';
import ProductService from '../../services/product-service';
const CartPage = () => {
const { id } = useParams();
const [productsInCart, setProductsInCart] = React.useState([]);
console.log('id from useParams', id);
React.useEffect(() => {
(async () => {
const fetchedProduct = await ProductService.fetchProductById(id);
setProductsInCart(fetchedProduct);
})();
}, [id]);
return (
<Container>
<pre>
{JSON.stringify(productsInCart, null, 4)}
</pre>
</Container>
);
};
export default CartPage;
Button which should add the product to the cart from ProductInformation component (it also logs the id in the console correctly):
<CustomButton
onClick={() => {
navigate('/cart');
addToCart({ id });
console.log(`add to cart ${id}`);
}}
>
Add to cart
</CustomButton>
Error that I can see in server's terminal when it breaks:
GET /api/products/632db73759073e4cb274e011?joinBy=categoryId&joinBy=woodTypeId 200 1178 -
336.686 ms
stringValue: '"undefined"',
kind: 'ObjectId',
value: 'undefined',
path: '_id',
reason: BSONTypeError: Argument passed in must be a string of 12 bytes or a string of 24 hex
characters or an integer
View model for the product:
const categoryViewModel = require('./category-view-model');
const woodTypeViewModel = require('./wood-type-view-model');
const productEverythingPopulatedViewModel = (productDocument) => ({
id: productDocument._id.toString(),
title: productDocument.title,
description: productDocument.description,
category: categoryViewModel(productDocument.categoryId),
price: productDocument.price,
img: productDocument.img,
woodType: woodTypeViewModel(productDocument.woodTypeId),
createdAt: productDocument.createdAt,
updatedAt: productDocument.updatedAt,
})
module.exports = productEverythingPopulatedViewModel;
Can anyone see the mistake I made?
Thanks!
You should add the param to the route like you did in product route.
<Route path="/cart/:id" element={<CartPage />} />
And then add the id to the navigate url.
<CustomButton onClick={() => {
navigate(`/cart/${id}`);
addToCart({ id });
console.log(`add to cart ${id}`); }}
> Add to cart </CustomButton>
I'm trying to build my homepage but I'm getting the below errors in my terminal and my react page is blank. Any suggestions on what I change to fix these errors? Thanks in advance
Line 4:8: 'Cart' is defined but never used
Line 7:27: 'Router' is defined but never used
Line 7:50: 'Link' is defined but never used
Line 17:8: 'page' is assigned a value but never used
Line 28:9: 'addToCart' is assigned a value but never used
import React, { useState } from "react";
import "./Homepage.css";
import Shop from "./Shop";
import Cart from "./Cart";
import About from "./About";
import ContactUs from "./ContactUs";
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
const PAGE_SHOP = "shop";
const PAGE_CART = "cart";
const PAGE_HOMEPAGE = "home";
export default function Homepage() {
const [cart, setCart] = useState([]);
const [page, setPage] = useState(PAGE_SHOP);
const navigateTo = (nextPage) => {
setPage(nextPage);
};
const getCartTotal = () => {
return cart.reduce((sum, { quantity }) => sum + quantity, 0);
};
const addToCart = (product) => {
let newCart = [...cart];
let itemInCart = newCart.find((item) => product.name === item.name);
if (itemInCart) {
itemInCart.quantity++;
} else {
itemInCart = {
...product,
quantity: 1,
};
newCart.push(itemInCart);
}
setCart(newCart);
};
return (
<div className="Header">
<header>
<button onClick={() => navigateTo(PAGE_CART)}>
Go to Cart ({getCartTotal()})
</button>
<button onClick={() => navigateTo(PAGE_SHOP)}>Shop</button>
</header>
<router>
<Routes>
<Route path="/" element={<PAGE_HOMEPAGE />} />
<Route path="/About" element={<About />} />
<Route path="/Shop" element={<Shop />} />
<Route path="/ContactUs" element={<ContactUs />} />
</Routes>
</router>
</div>
);
}
You have set const PAGE_HOMEPAGE = 'home';
If you look at the following line
<Route path="/" element={<PAGE_HOMEPAGE />} />
You are passing a string instead of an element.
Replace PAGE_HOMEPAGE with a react component that you would like to render on path '/'
To remove the warnings, just remove the code mentioned in those warnings, if you don't intend on using it.
If you do plan on using it, ignore the warnings for now and they will go away once the code is used.
I have the following problem: I have a general component that contains some data from the redux store and I want to clear this data once the user visits another route.
<Route path="/create/gallery" element={<CreatePage type={ContentType.gallery}/>} />
also I have some more code that saves my entered data to the store
saveGeneralInfo = (field: string, value: string) => {
const data = {};
data[field] = value;
this.props.dispatch(saveGeneralInfo(data));
}
How I can clear the state if the user leave the page or visit any other link? (For example from header)
if(this.state.keycloak) {
if(this.state.authenticated) return (
<div className="App">
<Header/>
<Routes>
<Route path="/" element={<Content />} />
<Route path="/sites"/>
<Route path="/users"/>
<Route path="/create/gallery" element={<CreatePage type={ContentType.gallery}/>}/>
<Route path="/create/article" element={<CreatePage type={ContentType.article} />} />
<Route path="/create/quiz" element={<CreatePage type={ContentType.quiz} />} />
</Routes>
</div>
);
else return (
<div>Can't authorize</div>
)
}
You will have to provide functionality for store clearing that fires on unMount lifecycle event in every route root component.
If you are using functional components:
export const Component = () => {
const dispatch = useDispatch();
useEffect(() => {
return () => {
dispatch(yourActionThatCleansReduxStore())
}
}, [])
//rest of your code
}
In my case I reset parts of my store for every page URL like /info or /user where store looks like
{
user: {
id: ...
},
info: ...
}
You can create a route controller using children component
import { useDispatch } from "react-redux";
import { useLocation } from "react-router-dom";
import { cleanState } from "Your-reducer.js";
function CleanState({ children }) {
const location = useLocation();
const dispatch = useDispatch();
useEffect(() => {
dispatch(cleanState()); // every time the route changes clean the state
// if you don't use redux-toolkit you can use action.payload etc....
},[location.pathname])
return <>{children}</>;
}
export default CleanState;
then you have to wrap the main component
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import YourComponents from "./YourComponents"; // Your components
import CleanState from "./CleanState"; // where did you save it
function App() {
return (
<Router>
<CleanState> // This is the previous component, this will listen the movements of the routes
<Routes>
<Route path="/main" element={<YourComponents />} />
<Route path="*" element={<YourComponents />} />
</Routes>
</CleanState>
</Router>
);
}
export default App;
I have a module in react project and want to use nested routes with params in the middle of url, but after clicking the link it redirects to not found page.
The current page url like this:
"/AssetContext/Transfers/123"
And the nested route on the same page is like this:
"/AssetContext/Transfers/123/NewTransferItem"
My module is like this:
import React, { Component, Suspense } from "react";
import TransferDetails from "./transfer-details";
import { Link, Route, Switch } from "react-router-dom";
const Test = React.lazy(() => import("./test"));
const TransferItems = React.lazy(() => import("./transfer-items"));
const NewTransferItem = React.lazy(() => import("./new-transfer-item"));
class TransferItemsContainer extends Component {
state = {
transferId: this.props.match.params.id
};
render() {
const { transferId } = this.state;
return (
<>
<TransferDetails transferId={transferId} />
<Link to={`/AssetContext/Transfers/${transferId}/test`}>test</Link>
<Link to={`/AssetContext/Transfers/${transferId}/TransferItems`}>Transfer Items</Link>
<Link to={`/AssetContext/Transfers/${transferId}/NewTransferItem`}>New Transfer Items</Link>
<Switch>
<Route
exact
path={`/AssetContext/Transfers/:transferId/test`}
component={Test}
/>
<Route
path="/AssetContext/Transfers/:transferId/TransferItems"
render={() => <TransferItems transferId={transferId} />}
/>
<Route
path="/AssetContext/Transfers/:transferId/NewTransferItem"
render={() => <NewTransferItem transferId={transferId} />}
/>
</Switch>
</>
);
}
}
export default TransferItemsContainer;
But the routes don't hit!
what is the problem?!
I'm looking for a way to modify the page title when React-Router v4+ changes locations. I used to listen for a location change action in Redux and check that route against a metaData object.
When using React-Router v4+, there's no fixed routes list. In fact, various components around the site could use Route with the same path string. That means old method I used won't work anymore.
Is there a way I can update the page title by calling actions when certain major routes are changed or is there a better a better method to update the site's metadata?
<Route /> components have render property. So you can modify the page title when location changes by declaring your routes like that:
<Route
exact
path="/"
render={props => (
<Page {...props} component={Index} title="Index Page" />
)}
/>
<Route
path="/about"
render={props => (
<Page {...props} component={About} title="About Page" />
)}
/>
In Page component you can set the route title:
import React from "react"
/*
* Component which serves the purpose of a "root route component".
*/
class Page extends React.Component {
/**
* Here, we define a react lifecycle method that gets executed each time
* our component is mounted to the DOM, which is exactly what we want in this case
*/
componentDidMount() {
document.title = this.props.title
}
/**
* Here, we use a component prop to render
* a component, as specified in route configuration
*/
render() {
const PageComponent = this.props.component
return (
<PageComponent />
)
}
}
export default Page
Update 1 Aug 2019. This only works with react-router >= 4.x. Thanks to #supremebeing7
Updated answer using React Hooks:
You can specify the title of any route using the component below, which is built by using useEffect.
import { useEffect } from "react";
const Page = (props) => {
useEffect(() => {
document.title = props.title || "";
}, [props.title]);
return props.children;
};
export default Page;
And then use Page in the render prop of a route:
<Route
path="/about"
render={(props) => (
<Page title="Index">
<Index {...props} />
</Page>
)}
/>
<Route
path="/profile"
render={(props) => (
<Page title="Profile">
<Profile {...props} />
</Page>
)}
/>
In your componentDidMount() method do this for every page
componentDidMount() {
document.title = 'Your page title here';
}
This will change your page title, do the above mentioned for every route.
Also if it is more then just the title part, check react-helmet It is a very neat library for this, and handles some nice edge cases as well.
Picking up from the excellent answer of phen0menon, why not extend Route instead of React.Component?
import React, { useEffect } from 'react';
import { Route } from 'react-router-dom';
import PropTypes from 'prop-types';
export const Page = ({ title, ...rest }) => {
useEffect(() => {
document.title = title;
}, [title]);
return <Route {...rest} />;
};
This will remove overhead code as seen below:
// old:
<Route
exact
path="/"
render={props => (
<Page {...props} component={Index} title="Index Page" />
)}
/>
// improvement:
<Page
exact
path="/"
component={Index}
title="Index Page"
/>
Update: another way to do it is with a custom hook:
import { useEffect } from 'react';
/** Hook for changing title */
export const useTitle = title => {
useEffect(() => {
const oldTitle = document.title;
title && (document.title = title);
// following line is optional, but will reset title when component unmounts
return () => document.title = oldTitle;
}, [title]);
};
Using a functional component on your main routing page, you can have the title change on each route change with useEffect.
For example,
const Routes = () => {
useEffect(() => {
let title = history.location.pathname
document.title = title;
});
return (
<Switch>
<Route path='/a' />
<Route path='/b' />
<Route path='/c' />
</Switch>
);
}
I built a bit on Thierry Prosts solution and ended up with the following:
UPDATE January 2020: I've now updated my component to be in Typescript as well:
UPDATE August 2021: I've added my private route in TypeScript
import React, { FunctionComponent, useEffect } from 'react';
import { Route, RouteProps } from 'react-router-dom';
interface IPageProps extends RouteProps {
title: string;
}
const Page: FunctionComponent<IPageProps> = props => {
useEffect(() => {
document.title = "Website name | " + props.title;
});
const { title, ...rest } = props;
return <Route {...rest} />;
};
export default Page;
UPDATE: My Page.jsx component is now a functional component and with useEffect hook:
import React, { useEffect } from 'react';
import { Route } from 'react-router-dom';
const Page = (props) => {
useEffect(() => {
document.title = "Website name | " + props.title;
});
const { title, ...rest } = props;
return <Route {...rest} />;
}
export default Page;
Below you can find my initial solution:
// Page.jsx
import React from 'react';
import { Route } from 'react-router-dom';
class Page extends Route {
componentDidMount() {
document.title = "Website name | " + this.props.title;
}
componentDidUpdate() {
document.title = "Website name | " + this.props.title;
}
render() {
const { title, ...rest } = this.props;
return <Route {...rest} />;
}
}
export default Page;
And my Router implementation looked like this:
// App.js / Index.js
<Router>
<App>
<Switch>
<Page path="/" component={Index} title="Index" />
<PrivateRoute path="/secure" component={SecurePage} title="Secure" />
</Switch>
</App>
</Router>
Private route setup:
// PrivateRoute
function PrivateRoute({ component: Component, ...rest }) {
return (
<Page
{...rest}
render={props =>
isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/",
state: { from: props.location }
}}
/>
)
}
/>
);
}
Private Route in TypeScript:
export const PrivateRoute = ({ Component, ...rest }: IRouteProps): JSX.Element => {
return (
<Page
{...rest}
render={(props) =>
userIsAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: Paths.login,
state: { from: props.location },
}}
/>
)
}
/>
);
};
This enabled me to have both public areas update with a new title and private areas also update.
With a little help from Helmet:
import React from 'react'
import Helmet from 'react-helmet'
import { Route, BrowserRouter, Switch } from 'react-router-dom'
function RouteWithTitle({ title, ...props }) {
return (
<>
<Helmet>
<title>{title}</title>
</Helmet>
<Route {...props} />
</>
)
}
export default function Routing() {
return (
<BrowserRouter>
<Switch>
<RouteWithTitle title="Hello world" exact={true} path="/" component={Home} />
</Switch>
</BrowserRouter>
)
}
Here is my solution which is almost the same as simply setting document.title but using useEffect
/**
* Update the document title with provided string
* #param titleOrFn can be a String or a function.
* #param deps? if provided, the title will be updated when one of these values changes
*/
function useTitle(titleOrFn, ...deps) {
useEffect(
() => {
document.title = isFunction(titleOrFn) ? titleOrFn() : titleOrFn;
},
[...deps]
);
}
This has the advantage to only rerender if your provided deps change.
Never rerender:
const Home = () => {
useTitle('Home');
return (
<div>
<h1>Home</h1>
<p>This is the Home Page</p>
</div>
);
}
Rerender only if my userId changes:
const UserProfile = ({ match }) => {
const userId = match.params.userId;
useTitle(() => `Profile of ${userId}`, [userId]);
return (
<div>
<h1>User page</h1>
<p>
This is the user page of user <span>{userId}</span>
</p>
</div>
);
};
// ... in route definitions
<Route path="/user/:userId" component={UserProfile} />
// ...
CodePen here but cannot update frame title
If you inspect the <head> of the frame you can see the change:
I am answering this because I feel you could go an extra step to avoid repetitions within your components and you could just get the title updated from one place (the router's module).
I usually declare my routes as an array but you could change your implementation depending on your style. so basically something like this ==>
import {useLocation} from "react-router-dom";
const allRoutes = [
{
path: "/talkers",
component: <Talkers />,
type: "welcome",
exact: true,
},
{
path: "/signup",
component: <SignupPage />,
type: "onboarding",
exact: true,
},
]
const appRouter = () => {
const theLocation = useLocation();
const currentLocation = theLocation.pathname.split("/")[1];
React.useEffect(() => {
document.title = `<Website Name> |
${currentLocation[0].toUpperCase()}${currentLocation.slice(1,)}`
}, [currentLocation])
return (
<Switch>
{allRoutes.map((route, index) =>
<Route key={route.key} path={route.path} exact={route.exact} />}
</Switch>
)
}
Another approach would be declaring the title already in each of the allRoutes object and having something like #Denis Skiba's solution here.
You also can go with the render method
const routes = [
{
path: "/main",
component: MainPage,
title: "Main Page",
exact: true
},
{
path: "/about",
component: AboutPage,
title: "About Page"
},
{
path: "/titlessPage",
component: TitlessPage
}
];
const Routes = props => {
return routes.map((route, idx) => {
const { path, exact, component, title } = route;
return (
<Route
path={path}
exact={exact}
render={() => {
document.title = title ? title : "Unknown title";
console.log(document.title);
return route.component;
}}
/>
);
});
};
the example at codesandbox (Open result in a new window for see title)
Please use react-helmet. I wanted to give the Typescript example:
import { Helmet } from 'react-helmet';
const Component1Title = 'All possible elements of the <head> can be changed using Helmet!';
const Component1Description = 'No only title, description etc. too!';
class Component1 extends React.Component<Component1Props, Component1State> {
render () {
return (
<>
<Helmet>
<title>{ Component1Title }</title>
<meta name="description" content={Component1Description} />
</Helmet>
...
</>
)
}
}
Learn more: https://github.com/nfl/react-helmet#readme
Dan Abramov (creator of Redux and current member of the React team) created a component for setting the title which works with new versions of React Router also.
It's super easy to use and you can read about it here:
https://github.com/gaearon/react-document-title
For instance:
<DocumentTitle title='My Web App'>