React Router: Link and Route not passing state to Component
I’m building an online shopping app with React, Redux, and React Router. On the home page, all items are displayed. I want users to be able to click on an image and be taken to a page with item details for that item.
Here's how I’m trying to do that:
In App.js, I’m using BrowserRouter, Route, and Switch for routing
In Home.js, I’m using react-router's Link to link an image on the Home page to an item details page; the Link is supposed to pass the ID to the Item component as state (see the Home.js code below)
Item.js is supposed to render an items detail page. The ID of the item will be passed to the Item component, which will render the details for that item.
This seems simple, and I’ve seen a lot of discussion about this on SO and across the web. Here are some that I have already tried:
Display item details after clicking Link in React Router
React router how to click thru to detail components
Passing values through React-Router v4 <Link />
Tyler McGinnis - Pass props to React Router's Link component
Medium - How to pass props to React routes components
I’m able to get the URL to change to correspond to the item’s ID, but I can’t get the Item component to render with the right information, and sometimes I get errors depending on the method I’m using. I hard coded the words "Item Component" inside a div in Item.js, and that will render when I click on the images from the Home page. Otherwise, nothing will render or I receive the two following TypeErrors:
App.js:
<Route path="/products" render={(props) => (<Item {...props} id={this.props.location.id} />) }/>
export default withRouter(App);
<App /> is wrapped in <BrowserRouter> in index.js.
I receive the following error when clicking on an image from the home page:
TypeError: Cannot read property 'props' of undefined
When the above “render” code is removed from and replaced with “component={ Item }”, I receive this error from Item.js:
TypeError: Cannot read property 'location' of undefined
Here are my other code snippets:
Item.js:
class Item extends React.Component {
constructor() {
super();
this.state = {
item: this.props.location.state
}
}
render() {
let item = this.state.item;
return (
<div className="item" key={item.id}>
<div className="item-image">
<img src={item.img} alt={item.title} />
</div>
<div className="card-component">
<span className="item-title">{item.title}</span>
<p className="item-price"><b>${item.price}</b></p>
<p className="item-desc">{item.desc}</p>
</div>
</div>
);
}
}
Home.js (where all items are displayed), the item details page is linked like so:
<Link to =
{{ pathname: `/products/${item.category}`,
search: `?id=${item.id}`,
state: {id: `${item.id}`} }}
component={ Item }>
<img src={item.img} alt={item.title} />
</Link>
UPDATE
I have wrapped the <Item/> component in withRouter().
In Item.js, if I set the state in the constructor as { item: this.props.location.state }, nothing renders as it should (only a dollar sign is on the screen, since the price section contains a hard coded dollar sign).
I tried setting the Item.js state to null in the constructor, then calling this.setState with { item: this.props.location.state } in componentDidMount(). When I do this, I receive the following error:
TypeError: Cannot read property 'img' of null
Also, if I leave the render={(props) => (<Item {...props} id={this.props.location.id} />) } in the <Route> in App.js, I receive this error:
TypeError: Cannot read property 'props' of undefined
If I change <Route> back to component={ Item }, and the Item.js constructor state is set to { item: this.props.location.state }, just the dollar sign renders.
UPDATE: include App.js and Routes.js code
App.js
import React from 'react';
import {
BrowserRouter as Router,
withRouter,
Switch
} from 'react-router-dom';
import './App.css';
import Navbar from './components/Navbar';
import Footer from './components/footer/Footer';
import { Routes } from './constants/Routes';
class App extends React.Component {
render() {
return (
<Router>
<div className="App">
<Navbar />
<Switch>
{ Routes }
</Switch>
<Footer />
</div>
</Router>
);
}
}
export default withRouter(App);
Routes.js
import React from 'react';
import { Route } from 'react-router-dom';
import Home from '../components/Home';
import Item from '../components/shopping/Item';
import Cart from '../components/cart/Cart';
import OrderStatus from '../components/footer/OrderStatus';
import GiftCards from '../components/footer/GiftCards';
import ReturnsExchanges from '../components/footer/Returns-Exchanges';
import Contact from '../components/footer/Contact';
export const Routes = <div>
<Route exact path="/" component={ Home } />
<Route path="/products" component={ Item }/>
<Route path="/cart" component={ Cart } />
<Route path="/order-status" component={ OrderStatus } />
<Route path="/gift-cards" component={ GiftCards } />
<Route path="/returns-exchanges" component={ ReturnsExchanges } />
<Route path="/contact" component={ Contact } />
</div>
One way to access history objects properties in any component, you need to wrap that component with withRouter.
import withRouter from react-router-dom and wrap your App component like this:
export default withRouter(App);
More on withRouter.
Also, in index.js, make sure your <App /> is wrapped inside <BrowserRouter>.
import { BrowserRouter } from 'react-router-dom';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>
, document.getElementById('root'));
EDIT:
TypeError: Cannot read property 'props' of undefined
Replace your constructor with the following code. When implementing a constructor for React Component super(props) should be before any other statement otherwise props will be undefined in constructor.
constructor(props){
super(props);
this.state = {
item: this.props.location.state
}
}
Error: TypeError: Cannot read property 'img' of null
Reason:
Initial value of the state should not be null. You may pass empty object {} but not null, in this case.
You're trying to access img, title, price and desc in Item.js but only passing id in state of <Link> in Home.js.
<Link to =
{{ pathname: `/products/${item.category}`,
search: `?id=${item.id}`,
state: {id: `${item.id}`} //pass img, title, desc, price etc as well
}}
component={ Item }>
<img src={item.img} alt={item.title} />
</Link>
Related
I am new to react and react-router, so please go easy on me.
I am trying to implement router in my Todo List project, where path="/" takes me to my todo list and path="/id" takes me to a test page (later will show the description of the task).
When I click the link that takes me to "/id", the URL in the browser changes but the page/content doesn't. However, when I refresh my browser, the test page loads.
I have put the Switch in App.js shown below.
import React, { Component } from "react";
import "./App.css";
import TodoList from "./components/TodoList";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Test from "./components/Test";
class App extends Component {
render() {
return (
<Router>
<div className="todo-app">
<p>
<Link to="/">Home</Link>
</p>
<Switch>
<Route exact path="/" component={TodoList} />
<Route path={`/id`} component={Test} />
</Switch>
</div>
</Router>
);
}
}
export default App;
And I have put the Link to "/id" as shown below in a child component of component which is called here in App.js.
<div key={todo.id}>
<Link className="todo-text" to={`/id/${todo.id}`}>
{todo.text}
</Link>
</div>
Am I missing something which is causing my component to not load when I click the link?
Edit: Here's a link to my project. https://stackblitz.com/edit/react-7cpjp9?file=src/index.js
Issue
Ok, the issue is exactly as I had suspected. You are rendering multiple routers in your app. The first is a BrowserRouter in your index.js file, the second, another BrowserRouter in App.js, and at least a third BrowserRouter in Todo.js. You need only one router to provide a routing context for the entire app.
The issue here is that the router in Todo component is the closest router context to the links to specific todo details. When a link in Todo is clicked, this closest router handles the navigation request and updates the URL in the address bar. The blocks, or "masks", the router in App component or index.js that is rendering the routes from "seeing" that a navigation action occurred. In other words, the URL in the address bar is updated by the inner router, but the outer router doesn't know to render a different route.
Solution
Keep the BrowserRouter wrapping App in index.js and remove all other routers used in your app.
App - Remove the Router component. Also, reorder the routes/paths from most specific to least specific so you don't need to specify the exact prop on every route. Allows more specific paths to be matched and rendered before less specific paths by the Switch component.
class App extends Component {
render() {
return (
<div className="todo-app">
<p>
<Link to="/">Home</Link>
</p>
<Switch>
<Route path="/id/:todoId" component={Test} />
<Route path="/" component={TodoList} />
</Switch>
</div>
);
}
}
Todo - Remove the Router component. Move the key={todo.id} up to the outer-most element so when todos array is updated React can reconcile updates.
class Todo extends Component {
constructor(props) {
super(props);
this.state = {
id: null,
value: "",
details: "",
};
this.submitUpdate = this.submitUpdate.bind(this);
}
submitUpdate(value) {
const { updateTodo } = this.props;
updateTodo(this.state.id, value);
this.setState({
id: null,
value: "",
});
}
render() {
const { todos, completeTodo, removeTodo } = this.props;
if (this.state.id) {
return <TodoForm edit={this.state} onSubmit={this.submitUpdate} />;
}
return todos.map((todo, index) => (
<div
className={todo.isComplete ? "todo-row complete" : "todo-row"}
key={todo.id}
>
<div>
<Link className="todo-text" to={`/id/${todo.id}`}>
{todo.text}
</Link>
</div>
<div className="icons">
<RiCloseCircleLine
onClick={() => removeTodo(todo.id)}
className="delete-icon"
/>
<TiEdit
onClick={() => this.setState({ id: todo.id, value: todo.text })}
className="edit-icon"
/>
<RiCheckboxCircleLine
onClick={() => completeTodo(todo.id)}
className="delete-icon"
/>
</div>
</div>
));
}
}
First of all the approach, you are taking for dynamic routing is wrong.
It should be like this you will have to add the exact keyword on the dynamic route.
<Route exact path="/id/:todoId" component={Test} />
And
<div key={todo.id}>
<Link className="todo-text" to={`/id/${todo.id}`}>
{todo.text}
</Link>
import React, { Component } from "react";
import "./App.css";
import TodoList from "./components/TodoList";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Test from "./components/Test";
class App extends Component {
render() {
return (
<Router>
<div className="todo-app">
<p>
<Link to="/">Home</Link>
</p>
<Switch>
<Route exact path="/" component={TodoList} />
**<Route exact path={`/id`} component={Test} />**
</Switch>
</div>
</Router>
);
}
}
export default App;
I have the following problem trying to hide the header, footer and aside for my login page
this is my code:
import React, { Component } from 'react';
import Header from './components/Header';
import Footer from './components/Footer';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import Routes from './routes';
export default class App extends Component {
render(){
return(
<div>
{this.location.pathname==='/Login' ? null:
<div>
<Header/>
<Routes />
<Footer />
</div>
}
</div>
)
}
}
I get this error message
TypeError: Cannot read property 'pathname' of undefined
How can I prevent my header, aside, footer from being shown on my login page?
Helpe please.
Issue(s)
pathname is undefined because you've not correctly accessed it from the (presumably) passed location prop from route props. You've missed the props part.
this.location.pathname
It also appears you are attempting to read Route state from App. In order for route props to work you need to wrap your App component in a Router higher up in the react tree and render App into a Route, wrap App with the withRouter Higher Order Component, or render a Router in App and render your header and footer into routes so they can check the location.
Solution
Render a Router around the app content. Render the header and footer into routes on the render prop and check the location.pathname and conditionally render each.
export default class App extends Component {
render() {
return (
<Router>
<Route
render={(props) =>
location.pathname !== "/Login" ? <Header /> : null
}
/>
<Routes />
<Route
render={(props) =>
location.pathname !== "/Login" ? <Footer /> : null
}
/>
</Router>
);
}
}
Alternative solution
Similar to above but defer the location.pathname check to the components. Render the header and footer on the component prop and check the passed route props.
export default class App extends Component {
render() {
return (
<Router>
<Route component={Header} />
<Routes />
<Route component={Footer} />
</Router>
);
}
}
Example check in Header
const Header = ({ location }) => {
...
return location.pathname !== "/login" ? (
... // header UI JSX
) : null;
};
I'm using React Router to create a multi page app. My main component is <App/> and it renders all of the routing to to child components. I'm trying to pass props via the route, and based on some research I did, the most common way for child components to tap into props passed down is via the this.props.route object that they inherit. However, this object is undefined for me. On my render() function in the child component, I console.log(this.props) and am return an object that looks like this
{match: Object, location: Object, history: Object, staticContext: undefined}
Doesn't look like the props I expected at all. Here is my code in detail.
Parent Component (I'm trying to pass the word "hi" down as a prop called "test" in all of my child components):
import { BrowserRouter as Router, HashRouter, Route, Switch } from 'react-router-dom';
import Link from 'react-router';
import React from 'react';
import Home from './Home.jsx';
import Nav from './Nav.jsx';
import Progress from './Progress.jsx';
import Test from './Test.jsx';
export default class App extends React.Component {
constructor() {
super();
this._fetchPuzzle = this._fetchPuzzle.bind(this);
}
render() {
return (
<Router>
<div>
<Nav />
<Switch>
<Route path="/" exact test="hi" component={Home} />
<Route path="/progress" test="hi" component={Progress} />
<Route path="/test" test="hi" component={Test} />
<Route render={() => <p>Page not found!</p>} />
</Switch>
</div>
</Router>
);
}
}
Child:
import React from 'react';
const CodeMirror = require('react-codemirror');
import { Link } from 'react-router-dom';
require('codemirror/mode/javascript/javascript')
require('codemirror/mode/xml/xml');
require('codemirror/mode/markdown/markdown');
export default class Home extends React.Component {
constructor(props) {
super(props);
console.log(props)
}
render() {
const options = {
lineNumbers: true,
theme: 'abcdef'
// mode: this.state.mode
};
console.log(this.props)
return (
<div>
<h1>First page bro</h1>
<CodeMirror value='code lol' onChange={()=>'do something'} options={options} />
</div>);
}
}
I'm pretty new to React so my apologies if I'm missing something obvious.
Thanks!
You can pass props to the component by making use of the render prop to the Route and thus inlining your component definition. According to the DOCS:
This allows for convenient inline rendering and wrapping without the
undesired remounting explained above.Instead of having a new React
element created for you using the component prop, you can pass in a
function to be called when the location matches. The render prop
receives all the same route props as the component render prop
So you can pass the prop to component like
<Route path="/" exact render={(props) => (<Home test="hi" {...props}/>)} />
and then you can access it like
this.props.test
in your Home component
P.S. Also make sure that you are passing {...props} so that the
default router props like location, history, match etc are also getting passed on to the Home component
otherwise the only prop that is getting passed down to it is test.
id like to change my header color when in my contact page. unable to get location from react router. I get "TypeError: Cannot read property 'pathname' of undefined".
I know I'm missing something simple but i cant seem to figure it out.
import React from 'react';
import './Header.css';
import { NavLink } from 'react-router-dom';
class Header extends React.Component {
render() {
const blackHeader = {"background-color" : 'black'}
const clearHeader = {"background-color" : 'transparent'}
return(
<header style={ this.props.location.pathname === '/Contact' ? { blackHeader } : { clearHeader } }>
<NavLink to={'/'}><h2>Name</h2></NavLink>
<nav>
<ul>
<li><NavLink to={'/'}>Portfolio</NavLink></li>
<li><NavLink to={'Contact'}>Contact</NavLink></li>
<li>Blog</li>
</ul>
</nav>
</header>
)
}
}
export default Header;
You need to pass the location props to your Header component or you can use the HOC withRouter in react-router-dom.
import { NavLink, withRouter } from 'react-router-dom';
export default withRouter(Header)
Edit for clarity:
If you render a component in your router in a Route component:
<Route path='/' component={Home} />
This will give that component (Home in this case) access to the location props which can then be passed to children of that component.
import React, { Component } from 'react'
import OtherComponent from './OtherComponent'
class Home extends Component {
render() {
return (
<div>
<OtherComponent locations={this.props.locations}
</div>
)
}
}
export default Home;
However, if the component is not passed through Route it will not have access to location props. For example, if you put your header outside of your routes, something like:
<Router>
<Header/>
<Switch>
<Route path='/' component={Home}/>
</Switch>
</Router>
It will not have location props.
I'm not sure how your component tree is set up but, I hope this gives a little clarity to how the location props are accessed and used in your app.
The documentation is a good place to get some more clarity on the workings of react router as well https://reacttraining.com/react-router/core/api/Route/route-props
Yea, I can see in the contact path
<li><NavLink to={'Contact'}>Contact</NavLink></li> ,
it should be something like
<li><NavLink to='/Contact'>Contact</NavLink></li>.
You have to include the back slash, I mean this ' / ' before the pathname. So try it now.
I'm using React Router to create a multi page app. My main component is <App/> and it renders all of the routing to to child components. I'm trying to pass props via the route, and based on some research I did, the most common way for child components to tap into props passed down is via the this.props.route object that they inherit. However, this object is undefined for me. On my render() function in the child component, I console.log(this.props) and am return an object that looks like this
{match: Object, location: Object, history: Object, staticContext: undefined}
Doesn't look like the props I expected at all. Here is my code in detail.
Parent Component (I'm trying to pass the word "hi" down as a prop called "test" in all of my child components):
import { BrowserRouter as Router, HashRouter, Route, Switch } from 'react-router-dom';
import Link from 'react-router';
import React from 'react';
import Home from './Home.jsx';
import Nav from './Nav.jsx';
import Progress from './Progress.jsx';
import Test from './Test.jsx';
export default class App extends React.Component {
constructor() {
super();
this._fetchPuzzle = this._fetchPuzzle.bind(this);
}
render() {
return (
<Router>
<div>
<Nav />
<Switch>
<Route path="/" exact test="hi" component={Home} />
<Route path="/progress" test="hi" component={Progress} />
<Route path="/test" test="hi" component={Test} />
<Route render={() => <p>Page not found!</p>} />
</Switch>
</div>
</Router>
);
}
}
Child:
import React from 'react';
const CodeMirror = require('react-codemirror');
import { Link } from 'react-router-dom';
require('codemirror/mode/javascript/javascript')
require('codemirror/mode/xml/xml');
require('codemirror/mode/markdown/markdown');
export default class Home extends React.Component {
constructor(props) {
super(props);
console.log(props)
}
render() {
const options = {
lineNumbers: true,
theme: 'abcdef'
// mode: this.state.mode
};
console.log(this.props)
return (
<div>
<h1>First page bro</h1>
<CodeMirror value='code lol' onChange={()=>'do something'} options={options} />
</div>);
}
}
I'm pretty new to React so my apologies if I'm missing something obvious.
Thanks!
You can pass props to the component by making use of the render prop to the Route and thus inlining your component definition. According to the DOCS:
This allows for convenient inline rendering and wrapping without the
undesired remounting explained above.Instead of having a new React
element created for you using the component prop, you can pass in a
function to be called when the location matches. The render prop
receives all the same route props as the component render prop
So you can pass the prop to component like
<Route path="/" exact render={(props) => (<Home test="hi" {...props}/>)} />
and then you can access it like
this.props.test
in your Home component
P.S. Also make sure that you are passing {...props} so that the
default router props like location, history, match etc are also getting passed on to the Home component
otherwise the only prop that is getting passed down to it is test.