Pass props to React Router's Link Component - reactjs

I'm passing location props to React Router's Link. It successfully navigates me away, but when I click "back" to go back to the previous page, I get an error:
TypeError Cannot Read property "statusUser" Undefined
It looks like the props are getting messed up when I navigate back.
Sidebar.js
Class Sidebar extends Commponent{
render(){
return(
<Link to={
{
pathname: `/user`,
state:{
statusUser: 'abc',
}
}
}><Button>User Menu</Button>
}
)
}
User.jsx
class user extends Component {
render(){
return(
<UserTable status={this.props.location.state.statusUser}/>
)
}
}
UserTable.jsx
class UserTable extends Component{
render(){
return(
<Link to={
{
pathname: `/user/detail/${this.props.status}`,
state:{
statusUser: this.props.status,
}
}
}><Button>Detail</Button>
)
}
}
UserDetail.jsx
class UserDetail extends Component{
render(){
return(
<p>{this.props.location.state.statusUser}</p>
<Link to={
{
pathname: `/user`,
}
}><Button>Back</Button>
)
}
}
I think the problem is in User.js. How do I make the props fixed like using setState or stuff like that inside User.js file?
Just to be able read the props property, and after reading it go back to the previous page by clicking "back" button.
I'm sorry I just really really new to React so any help would be really appreciate. Thank you.

I believe you're getting that error when you click the Back button in UserDetail.jsx. You aren't passing a state object to User.jsx.
The User.jsx component tries to access this.props.location.state.statusUser, but state doesn't exist, and so it says it can't read the property statusUser on undefined (state is undefined).
Here's what you're missing, with a few typos fixed:
// UserDetail.jsx
class UserDetail extends Component {
render() {
return (
<div>
<p>{this.props.location.state.statusUser}</p>
<Link
to={{
pathname: `/user`
// (You aren't passing `state`)
}}
>
<Button>Back</Button>
</Link> // <-- You weren't closing Link
</div>
);
}
}
// User.jsx
class User extends Component { // <-- Capital 'U'
render() {
return (
<UserTable
status={this.props.location.state.statusUser} // <-- You require `state`
/>
);
}
}
As for how to fix it, the easiest way would be to pass a state object with your "Back" button in UserDetail.jsx.
You can also set defaultProps in React. I'm not sure if that's applicable to you, but it lets you provide a fallback for such things:
Edit: Include example of defaultProps and propTypes.
// User.jsx
class User extends Component {
render() {
return (
<UserTable
status={this.props.location.state.statusUser}
/>
);
}
}
// Maybe something you put in a different file
const defaultLocation = {
state: {
statusUser: '', // <-- Your default statusUser
},
};
User.defaultProps = {
location: defaultLocation,
};
// import PropTypes from 'prop-types';
User.propTypes = {
location: PropTypes.shape({
state: PropTypes.shape({
statusUser: PropTypes.string.isRequired,
})
})
};
I added PropTypes in the above example. It's a way to set checks for the data you're requiring in your components.
Typically, we don't put props into defaultProps if it isRequired, though, because it will never come to that. If you're unsure what default value to give statusUser, I'd recommend starting with making it required in propTypes, and then you can refactor in the future if the need arises.
There were some bugs with your other code samples, too. I've formatted them below, and fixed the bugs. I'll point out what I fixed in comments:
Sidebar.js
class Sidebar extends Component { // <-- Lowercase 'c'; "Component"
render() {
return (
<Link
to={{
pathname: `/user`,
state: {
statusUser: "abc"
}
}}
>
<Button>User Menu</Button>
</Link> // <-- You weren't closing Link
);
}
}
UserTable.jsx
class UserTable extends Component {
render() {
return (
<Link
to={{
pathname: `/user/detail/${this.props.status}`,
state: {
statusUser: this.props.status
}
}}
>
<Button>Detail</Button>
</Link> // <-- You weren't closing Link
);
}
}

Related

React Component crashes on reload

I have a notes-list component that gets the notes data as props from the main component.
Inside the notes-listcomponent, there is a notes-item component which has a dynamic route that loads notesItem-page. So, for every notes-item there is a dynamic url for it's respective notesItem-pagewhich has all the details about the notesItem object. I use Link from react-router-dom
The notes-list component looks like this:
export class NotesList extends Component {
constructor(props) {
super(props);
}
render() {
const { isLoggedIn } = this.props.loggedInContext;
return (
<div className="notes-list">
{this.props.notes.map((notesItem) => (
<Link
to={{
pathname: `${notesItem.slug}`,
id: notesItem._id,
}}
style={{
textDecoration: "none",
color: "#fea82f",
}}
>
<NotesItem notes={notesItem} />
</Link>
))}
</div>
);
}
}
export default loggedInContext(NotesList);
This successfully redirects me to the NotesItem-page with the correct props and inside the notesItem-page I get the receive the id of the object that I had passed as props and make an API call with that particular id in ComponentDidMount() method.
This works perfectly. However, it crashes on reload. It gives the following error:
I am guessing it is because of ComponentDidMount works only once,but I do not seem to find an alternate solution to this problem.
The notesItem-page component looks like this:
export class notesItemPage extends Component {
constructor(props) {
super(props);
this.state = {
notesItem: [],
};
}
componentDidMount() {
fetch(`http://localhost:5000/api/v1/notes/fetch/${this.props.location.id}`)
.then((notesItem) => notesItem.json())
.then((notesItem) =>
this.setState({ notesItem: notesItem.data, isLoaded: true })
);
}
render() {
const { notesItem } = this.state;
return (
<div className="notesItem-details">
<h1> {notesItem.title} Notes</h1>
</div>
);
}
}
export default notesItemPage;
It would be great if anyone could help me with this, thanks!
Issue is here:
<h1> {notesItem.title} Notes</h1>
here the notesItem is coming from an axios call and this data is not available on the time of first component render and this is causing the issue ( app crash ).
So change this:
<h1> {notesItem.title} Notes</h1>
to
<h1> { notesItem && notesItem.title } Notes</h1> // use it when it is available from axios call

sending props via the react-router Link component

I have a problem with sending props through the Link component of the react-router.
This is my component that is responsible for displaying a specific element:
const ItemRecipe = ({ recipe }) => {
const { label, image } = recipe.recipe;
return (
<li>
<p> {label} </p>
<img src={image} alt="food" />
<Link to={{pathname: `/meals/${label}`, params: recipe }}>
<p>next</p>
</Link >
</li>
);
}
After clicking on I want to open the page with a specific recipe. This component looks like this
class DetailsRecipe extends Component {
constructor(props) {
super(props);
this.state = {
recipe: props.match.params.recipe
}
console.log(this.state.recipe);
}
render () {
<div>
lorem lorem
</div>
)
}
}
console.log(this.state.recipe) displays undefined.
how to fix this error? How to correctly send data through the Link component of the react-router?
I looked at similar topics on a stackoverflow but it did not help to solve my problem
Have another look at the documentation of the Link component. The properties allowed inside the to object are:
pathname: A string representing the path to link to.
search: A string representation of query parameters.
hash: A hash to put in the URL, e.g. #a-hash.
state: State to persist to the location.
I guess you want to use state instead of params.
EDIT:
So your code would look like this:
<Link to={{pathname: `/meals/${label}`, state: {"recipe": recipe} }}>
Then you can access the state inside the linked component like this:
this.state = {
recipe: props.location.state.recipe
};

Trying to dynamically select components in React

I'm super new to React and I have two components I want to toggle between based on a user click. I've went about it by creating a 'currentView' state and using that to set/update what the user should be looking at, but when I try to get the components to display it throws tag errors. Here's my code:
class App extends Component {
constructor(props){
super(props);
this.state={
currentView: "Welcome",
};
}
goTrack() {
this.setState({currentView: "Tracker"});
console.log(this.state.currentView);
}
goReview() {
this.setState({currentView: "Review"});
console.log(this.state.currentView);
}
render() {
return (
<div className="App">
<aside>
<nav>
<ul>
<li onClick={()=>this.goTrack()}> Tracker </li>
<li onClick={()=>this.goReview()}> Review </li>
</ul>
</nav>
</aside>
<main>
<this.state.currentView/>
</main>
</div>
);
}
}
export default App;
My question is how should I go about dynamically selecting components to display without re-rendering the entire DOM?
One way to solve this is to use the current state to match a key in an object containing the component you want in a certain state.
constructor() {
super()
this.state = {
current: 'welcome',
}
}
render() {
const myComponents = {
welcome: <Welcome />,
tracker: <Tracker />,
}
const CurrentComponent = myComponents[this.state.current]
return (
<CurrentComponent />
)
}
And when you change the state current with the value 'tracker', Tracker component will be rendered instead of Welcome component.
I am guessing Tracker and Review are two components you want to toggle based on value in this.state.currentView
In short this <this.state.currentView/> expects a component/html element.
one way to do what you want to do would be to do this instead.
<main>
{ this.state.currentView == 'Review' && (
<Review />
)}
{ this.state.currentView == 'Tracker' && (
<Tracker />
)}
</main>
Option 1: Conditions
render() {
const { currentView } = this.state;
// I omit the other stuff to focus on your question
return (
<div>
{currentView === 'Welcome' && <Welcome />}
{currentView === 'Tracker' && <Tracker />}
{currentView === 'Review' && <Review />}
</div>
);
}
Option 2: Dynamic component
import Welcome from './Welcome';
import Review from './Review';
class App extends Component {
constructor(props) {
super(props);
this.state = {
current: Welcome,
};
}
// ... stuff ...
goTrack() {
this.setState(prevState => { ...prevState, current: Review });
}
// ... other stuff ...
render() {
# I rename it because React expects a component's name
# with a capital name.
const { current: Current } = this.state;
# As above, I put only relevant rendering
return <div><Current /></div>;
}
}
Option 3: Guess
And actually, looking at what you are trying to do, I'd suggest you have a look at react-router-dom.

ReactJS - JSON objects in arrays

I am having a little problem and can't seem to understand how to fix it. So I am trying to create a pokemon app using pokeapi. The first problem is that I can't get my desired objects to display. For example I want to display {pokemon.abilities[0].ability}, but it always shows Cannot read property '0' of undefined but just {pokemon.name} or {pokemon.weight} seems to work. So the problem appears when there is more than 1 object in an array.
import React, {Component} from 'react';
export default class PokemonDetail extends Component{
constructor(props){
super(props);
this.state = {
pokemon: [],
};
}
componentWillMount(){
const { match: { params } } = this.props;
fetch(`https://pokeapi.co/api/v2/pokemon/${params.id}/`)
.then(res=>res.json())
.then(pokemon=>{
this.setState({
pokemon
});
});
}
render(){
console.log(this.state.pokemon.abilities[0]);
const { match: { params } } = this.props;
const {pokemon} = this.state;
return (
<div>
{pokemon.abilities[0].ability}
<img src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${params.id}.png`} />
</div>
);
}
}
And also some time ago I added the router to my app, so I could pass id to other components, but the thing is I want to display pokemonlist and pokemondetail in a single page, and when you click pokemon in list it fetches the info from pokeapi and display it in pokemondetail component. Hope it makes sense.
import React, { Component } from 'react';
import { BrowserRouter as Router, Route} from 'react-router-dom';
import './styles/App.css';
import PokemonList from './PokemonList';
import PokemonDetail from './PokemonDetail';
export default class App extends Component{
render(){
return <div className="App">
<Router>
<div>
<Route exact path="/" component={PokemonList}/>
<Route path="/details/:id" render={(props) => (<PokemonDetail {...props} />)}/>
</div>
</Router>
</div>;
}
}
In case componentWillMount(), An asynchronous call to fetch data will not return before the render happens. This means the component will render with empty data at least once.
To handle this we need to set initial state which you have done in constructor but it's not correct. you need to provide default values for the abilities which is an empty array.
So change it to
this.state = {
pokemon: {
abilities: []
}
}
And then inside render method before rendering you need to verify that it's not empty
render() {
return (
(this.state.pokemon.abilities[0]) ?
<div>
{console.log(this.state.pokemon.abilities[0].ability)}
<img src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png`} />
</div> :
null
);
}
It is common in React that you always need to safe-check for existence of data before rendering, especially when dealing with API data. At the time your component is rendered, the state is still empty. Thus this.state.pokemon.abilities is undefined, which leads to the error. this.state.pokemon.name and this.state.pokemon.weight manage to escape same fate because you expect them to be string and number, and don't dig in further. If you log them along with abilities, they will be both undefined at first.
I believe you think the component will wait for data coming from componentWillMount before being rendered, but sadly that's not the case. The component will not wait for the API response, so what you should do is avoid accessing this.state.pokemon before the data is ready
render(){
const { match: { params } } = this.props;
const {pokemon} = this.state;
return (
<div>
{!!pokemon.abilities && pokemon.abilities[0].ability}
<img src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${params.id}.png`} />
</div>
);
}

React doesn't change to LoggedInView when Meteor user logs in

I am developing a React + Meteor application and I'm having trouble with the user login functionality.
I have a header navbar that displays a different component based on whether or not the user is logged in.
Like this:
export default class Header extends Component {
constructor(props) {
super(props)
this.state = {
user: Meteor.user()
}
}
render() {
return (
<header className="main-header">
<nav className="navbar navbar-static-top">
<div className="navbar-custom-menu">
{this.state.user() !== null ? <LoggedInNavigation /> : <LoggedOutNavigation />}
</div>
</nav>
</header>
)
}
}
Now this works but it doesn't change upon a user being logged in. I have to refresh the page in order to change the views (which obviously is not ideal).
Here is my login code:
Meteor.loginWithPassword(this.state.email, this.state.password, (error) => {
if (error)
this.setState({ meteorError: "Error: " + error.reason })
else {
this.setState({ meteorError: "" })
// Handle successful login
}
})
The problem is these two blocks of code sit in different components.
The first block is in imports/ui/components/main-layout/Header and the second block is in imports/ui/components/authentication/Login.
As I said, the problem is that the user can log in but the view doesn't change according to the authentication state. What's the best practice to solving this?
EDIT:
Here is the hierarchy of components:
1 - LoggedOutNav
MainLayout -> Header -> LoggedOutNav
2 - Login Code
MainLayout -> Routes -> (Route path="/login" component={Login}) -> LoginForm
The problem here is that the constructor of your class will only run once and never again as long as the component is mounted. So even though Meteor.user() will change, your state won't. The component will rerender when a) the props change or b) your state changes e.g. when you call setState. We can leverage a) through meteors createContainer HOC (react-meteor-data) to wrap your Header class and set a reactive data context for it. When the data changes, the props for Header will change and the component rerenders. In code that would be something like:
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
import React, { Component, PropTypes } from 'react';
class HeaderComponent extends Component {
render() {
const { user } = this.props;
return (
<header className="main-header">
<nav className="navbar navbar-static-top">
<div className="navbar-custom-menu">
{user ? <LoggedInNavigation /> : <LoggedOutNavigation />}
</div>
</nav>
</header>
)
}
}
export const Header = createContainer(() => {
// assuming you have a user publication of that name...
Meteor.subscribe('users/personalData');
return {
user: Meteor.user(),
};
}, HeaderComponent);

Resources