Reducer Action Not Dynamically Updating - reactjs

I'm creating a basic CRUD app using React/Redux with a Rails API, and when I submit a car on my car-form, I get an error message - but refreshing the browser shows the car.
The error says Uncaught TypeError: Cannot read property 'map' of undefined on line 20 of my Cars.js file:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import CarCard from '../components/CarCard';
import CarForm from './CarForm';
import './Cars.css';
import { getCars } from '../actions/cars';
class Cars extends Component {
componentDidMount() {
this.props.getCars()
}
render() {
return (
<div className="CarsContainer">
<h3>Cars Component</h3>
{this.props.cars.cars.map(car => <CarCard key={car.id} car={car} />)}
<CarForm />
</div>
);
}
}
const mapStateToProps = (state) => {
return ({
cars: state.cars
})
}
export default connect(mapStateToProps, { getCars })(Cars);
Here's my createCar action creator:
const addCar = car => {
return {
type: 'CREATE_CAR_SUCCESS',
car
}}
And my createCar async action:
export const createCar = car => {
return dispatch => {
return fetch(`${API_URL}/cars`, {
method: "POST",
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({ car: car })
})
.then(response => {
try {
return response.json()
} catch(error) {
console.log(error);
}
})
.then(cars => {
dispatch(addCar([car]));
dispatch(resetCarForm())
})
.catch(error => console.log(error + 'createCar POST failed'))
}}
I'm not sure what's going wrong here, seeing as the app reflects my changes after I reload. Ultimately I'm trying to show that information without having to refresh the page.

The problem is that when your component mounts, it doesn’t have the cars array and instead it has an undefined value.
This happens because getCars() is asynchronous.
Solution 1: add a defaultProp to the component:
Component.defaultProps = {
cars: { cars: [] }
}
Solution 2:
Add a cars key to the reducer’s initialState
initialState: { cars:{ cars:[] } }

You are rendering before your async action puts the values in the state. Try returning null from render if your state is not set yet:
render() {
if(!this.props.cars.cars){
return null;
}
return (
<div className="CarsContainer">
<h3>Cars Component</h3>
{this.props.cars.cars.map(car => <CarCard key={car.id} car={car} />)}
<CarForm />
</div>
);
}
In other words, if your state does not have a list of things to render return null - I think the above if will work, but you might want to console.log("Cars in render", this.props.cars) to see what you are getting.
The better option, IMO, is to set your initial state so that this.props.cars is [] and then you don't have to return null and have a special case in your render method. I would need to see your reducer to suggest how to do that, but if you make it have a sensible default/initial state you should be able to easily do this.

You are doing action call getCars in componentDidMount and this lifecycle method gets called after first render so on Initial render this.props.cars will be undefined
If you are getting this.props.cars like
{
“cars”: [....]
}
Then you need to do conditional check before accessing cars object
Change
{this.props.cars.cars.map(car => <CarCard key={car.id} car={car} />)}
To
{this.props.cars && this.props.cars.cars.map(car => <CarCard key={car.id} car={car} />)}

Related

TypeError: Cannot read property 'map' of undefined When trying to map multiple arrays

I am trying to map multiple arrays at the same time and im not sure if this is how you do it. I am getting the error
TypeError: Cannot read property 'map' of undefined
When trying the following code
import React, { Component } from 'react';
import Axios from 'axios';
import NavBar from '../header-footer/nav-bar'
import Featured from './FeaturedMealplan'
import RecipeItem from './RecipeItem'
export default class MealPlanDetail extends Component {
constructor(props) {
super(props);
this.state = {
currentId: this.props.match.params.slug,
mealplanItem: {}, // Full mealplan
mealplanRecipes: [], // Contains recipe names and difficulty.
}
}
getMealplanItem() {
Axios.get(`http://localhost:5000/get-mealplan/${this.state.currentId}`
).then(response => {
console.log("response", response)
this.setState({
mealplanItem: response.data.mealplan,
mealplanRecipes: this.state.mealplanRecipes.concat(response.data.mealplan["recipes"]),
mealplanIngredients: this.state.mealplanIngredients.concat(response.data.mealplan["recipe_info"]),
recipeItem: response.data.mealplan.recipes
})
}).catch(error => {
console.log("mealplan-detail GET Error ", error)
})
}
componentDidMount() {
this.getMealplanItem();
}
render() {
const renderRecipe = this.state.recipes.map((recipe, idx) => {
return (
<div key={idx}>
<h1>{recipe.recipe_name}</h1>
<h2>Recipe Difficulty: <span>{recipe.recipe_dificulty}</span></h2>
<div>
<RecipeItem recipeItem={this.state.recipeItem} />
</div>
</div>
)
})
return (
<div>
<NavBar/>
<Featured/>
{renderRecipe}
</div>
)
}
}
Data that is given: https://pastebin.com/uYUuRY6U
I just need to be able to format it correctly which this is how I would like it formatted in the renderRecipe return. I am new to mapping and do not know if there is a way to fix or a better way.
Some issues in the code that we can improve on:
this.state.recipes seems to be undefined in your logic. Is it a typo?
I would suggest implementing renderRecipe as a function instead of a variable.
You would only hope to render renderRecipe when there is data, but when your component is being mounted, this.state.recipes is undefined. It would only have value when getMealplanItem gets a response and being defined in the callback. So you should check whether the value is defined before rendering.
Please refer to my comments in the code below.
import React, { Component } from "react";
import Axios from "axios";
import NavBar from "../header-footer/nav-bar";
import Featured from "./FeaturedMealplan";
import RecipeItem from "./RecipeItem";
export default class MealPlanDetail extends Component {
constructor(props) {
super(props);
this.state = {
// ... define `recipes` if that's what you want
};
}
getMealplanItem() {
Axios.get(`http://localhost:5000/get-mealplan/${this.state.currentId}`)
.then((response) => {
console.log("response", response);
// ... set state `recipes` here if that's what you want
this.setState({
mealplanItem: response.data.mealplan,
mealplanRecipes: this.state.mealplanRecipes.concat(
response.data.mealplan["recipes"]
),
mealplanIngredients: this.state.mealplanIngredients.concat(
response.data.mealplan["recipe_info"]
),
recipeItem: response.data.mealplan.recipes
});
})
.catch((error) => {
console.log("mealplan-detail GET Error ", error);
});
}
componentDidMount() {
this.getMealplanItem();
}
render() {
const renderRecipe = () => {
// change renderRecipe from a variable to a function
if (!this.state?.recipes) {
// check whether `recipes` is a defined value
return null;
}
return this.state.recipes.map((recipe, idx) => {
return (
<div key={idx}>
<h1>{recipe.recipe_name}</h1>
<h2>
Recipe Difficulty: <span>{recipe.recipe_dificulty}</span>
</h2>
<div>
<RecipeItem recipeItem={this.state.recipeItem} />
</div>
</div>
);
});
};
return (
<div>
<NavBar />
<Featured />
{renderRecipe()} // it's a function call now
</div>
);
}
}
There is never a this.state.recipes defined. Based on data type and comment
this.state = {
currentId: this.props.match.params.slug,
mealplanItem: {}, // Full mealplan
mealplanRecipes: [], // Contains recipe names and difficulty.
}
I will assume you meant for it to really be this.state.mealplanRecipes.
Your render then becomes
const renderRecipe = this.state.mealplanRecipes.map((recipe, idx) => {...
This can easily handle the initial render with an empty array.

When looping through an array of objects I get a null value error

I am fetching data from the NHL API (using react, redux, and fetch api), and trying to display a teams roster information. When I console log the roster api response in the component, it works correctly and displays the array of objects, but when I try and loop through it, I get an error that the response in null.
Here is what the rosterList which holds the response looks like: https://gyazo.com/3f14d24b3a5bfb3610b470abedb6fcca
Here is the redux action:
// Redux action
export const getRoster = id => async dispatch => {
try {
setLoading();
const res = await fetch(
`https://statsapi.web.nhl.com/api/v1/teams/${id}?expand=team.roster`
);
const data = await res.json();
dispatch({
type: GET_TEAM_ROSTER,
payload: data['teams'][0].roster.roster
});
} catch (err) {
dispatch({
type: TEAM_ERROR,
payload: err.response
});
}
};
And the redux state:
case GET_TEAM_ROSTER:
return {
...state,
currentRoster: action.payload,
loading: false
};
And finally the component in which I am trying to render the list:
import React, { useEffect, useState } from 'react';
import styles from './teamroster.module.css';
import { getRoster } from '../../actions/teamActions';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
const TeamRoster = props => {
const id = props.teamId;
let rosterList = props.team.currentRoster;
console.log(rosterList);
useEffect(() => {
props.getRoster(id);
// eslint-disable-next-line
}, []);
return (
<div className={styles.rosterContainer}>
<p>This is the team roster</p>
<ul>
/*********** Error here, cannot read length of null ********/
{!props.loading && rosterList.length === 0 ? (
<p>No players available</p>
) : (
rosterList.map((player, index) => (
<li key={index}>{player.jerseyNumber}</li>
))
)}
</ul>
</div>
);
};
TeamRoster.propTypes = {
getRoster: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
team: state.team
});
export default connect(
mapStateToProps,
{ getRoster }
)(TeamRoster);
I can't figure out what the problem is, and would appreciate any guidance. Thanks.
rosterList.length is returning undefined because the component is rendering for the first time before rosterList is returned from the API. Add a check to ensure that rosterList exists before trying to call '.length' on it.
The code would look like this:
<ul>
{props.loading || !rosterList ? (
<p>No players available</p>
) : (
rosterList && rosterList.map((player, index) => (
<li key={index}>{player.jerseyNumber}</li>
))
)}
</ul>

React this.props.id is undefined in part of class

In my React application I need the userId in the Timeline class to get the posts from a user, but React says that it's undefined.
If I say in the rendered part
{ this.props.id }
Than it will show the right id..
I already tried every solution that I could possibly find on the internet.
import React, { Component } from 'react'
import axios from 'axios'
import Timeline from './Timeline'
class Profile extends Component{
state = {
user: {}
}
componentDidMount() {
axios.get(`http://localhost:8090/user/${this.props.match.params.id}`)
.then(res =>{
const user = res.data
this.setState({ user: user })
})
}
render(){
return(
<div>
<h1>This is the profile page of { this.state.user.username }.</h1>
<img src={this.state.user.profilePicture} ></img>
<h3> E-mailaddress: { this.state.user.mail }</h3>
<Timeline id={this.state.user.id}/>
</div>
)}
}
export default Profile
import Cookies from 'universal-cookie'
import React, { Component } from 'react'
import axios from 'axios'
const cookies = new Cookies()
class Timeline extends Component {
state = {
user: cookies.get('user'),
posts: []
}
componentDidMount() {
const id = this.props.id
console.log("ID IS " + id)
if (this.state.user === undefined)
return
axios.get(`http://localhost:8090/user/${id}/postEntities`)
.then(response => {
this.setState({
posts: response.data._embedded.post
})
})
.catch(error => {
console.log(error)
})
}
render() {
if (this.state.user !== undefined) {
if (this.state.posts.length <= 0) {
return (
<main>
<h2>Personal timeline</h2>
<h2>This id works: { this.props.id }</h2>
<h6>There does not seem to be anything here..<br />Create a post and come back later!</h6>
</main>
)
} else {
return (
<main>
<h2>Personal timeline</h2>
{
this.state.posts.map(post => {
return (
<div>
<h5>{ post.title }</h5>
<img src={post.pictureUrl} width="200" height="200"></img>
<p><i>You took this picture at { post.longitude }, { post.latitude }</i></p>
</div>
)
})
}
</main>
)
}
}
else {
return (
<h5>You need to be logged in to use this feature</h5>
)
}
}
}
export default Timeline
The expected output in the url needs to be 2 but is undefined, the expected value in the rendered part is 2 and it outputs 2.
With react, the componentDidMount of children is called BEFORE the one from the parent.
So, when the componentDidMount of Timeline is called the first time, the componentDidMount of Profile has not been called, so there is no userId yet.
To avoid this problem, you should render the Timeline only when the Profile component has been mounted and when you have your user id.
So something like that in the render of Profile
render(){
return(
<div>
<h1>This is the profile page of { this.state.user.username }.</h1>
<img src={this.state.user.profilePicture} ></img>
<h3> E-mailaddress: { this.state.user.mail }</h3>
{this.state.user.id && (
<Timeline id={this.state.user.id}/>
)}
</div>
)}
Because
this.state.user.id
only has value when function axios.get in componentDidMount has done. while function render() is called before.
So, To avoid undefined, you must set state with format:
state = {
user: {id : 0} //or null
}
Initially you won't have user.id, it is coming from axios service call. In this case wait till you get response and then show timeline based on condition in render.
import React, { Component } from 'react'
import axios from 'axios'
import Timeline from './Timeline'
class Profile extends Component{
state = {
user: {}
}
componentDidMount() {
axios.get(`http://localhost:8090/user/${this.props.match.params.id}`)
.then(res =>{
const user = res.data
this.setState({ user: user })
})
}
render(){
return(
<div>
<h1>This is the profile page of { this.state.user.username }.</h1>
<img src={this.state.user.profilePicture} ></img>
<h3> E-mailaddress: { this.state.user.mail }</h3>
{typeof(this.state.user.id) !== 'undefined' ? <Timeline id={this.state.user.id}/> : ''}
</div>
)}
}
export default Profile
What variable is undefined? this.state.user.id?
If so, that probably means that you start with user: {}, then you make a promise and then set the state. The problem is that a promise will take time to fulfill, so meanwhile you are still with user: {} and this.state.user.id gives undefined.
When you call <Timeline id={this.state.user.id}/> make sure you have a id and email in your state. Or define your state with user: {is: '', email:''} or do a conditional render. Hope I understood your problem correctly!

How to fix recursively updating state?

I am bulding an app using newsapi. i am facing two issue on my state. i fetch data using api and assign it to my state. and use it in my view.
Issue no 1
My view gets rendered before my app receives the data.
Issue no 2
When I try to update my state after a new fetch. it recursively updates the set of data again and again.
import React, {Component} from 'react';
import NewsComponent from './NewsComponent/NewsComponent'
class News extends Component {
state = {
displayStatus: false,
newsItems: []
};
toogleDisplayHandler = () => {
if(this.state.displayStatus===true){
this.setState({displayStatus:false})
}
else{
this.setState({displayStatus:true})
}
}
render(){
const NewsAPI = require('newsapi');
const newsapi = new NewsAPI('d6da863f882e4a1a89c5152bd3692fb6');
//console.log(this.props.keyword);
newsapi.v2.topHeadlines({
sources: 'bbc-news,abc-news',
q: this.props.keyword
}).then(response => {
//console.log(response)
response.articles.map(article => {
//console.log(article);
return(
//console.log(this.state.newsItems)
this.setState({
newsItems: [...this.state.newsItems, article],
})
//this.state.newsItems.push(article)
)
});
});
let Article = null;
Article = (
<div>
{
this.state.newsItems.map((news, index) => {
return (
<NewsComponent key={index}
title={news.title}
url={news.url}
description={news.description}
author={news.author}
publish={news.publishedAt}
image={news.urlToImage}
/>
)
})
}
</div>
)
return (
<div className="App">
{Article}
<button onClick={this.toogleDisplayHandler}>
{this.state.displayStatus === true ? "Hide Article" : "Display Articles"}
</button>
</div>
)
}
}
export default News;
Please help me to resolve this issue.
You should never setState in render as that would cause an infinite loop. Do it in componentDidMount or the constructor.
I would also recommend not using map for simply iterating over a list. Array.map is a function that is useful for returning an array that is constructed by iterating over another array. If you want to run some code for each element of an array use Array.forEach instead.
Like this:
import React, { Component } from "react";
import NewsComponent from "./NewsComponent/NewsComponent";
class News extends Component {
state = {
displayStatus: false,
newsItems: []
};
toogleDisplayHandler = () => {
if (this.state.displayStatus === true) {
this.setState({ displayStatus: false });
} else {
this.setState({ displayStatus: true });
}
};
componentDidMount = () => {
const NewsAPI = require("newsapi");
const newsapi = new NewsAPI("d6da863f882e4a1a89c5152bd3692fb6");
newsapi.v2
.topHeadlines({
sources: "bbc-news,abc-news",
q: this.props.keyword
})
.then(response => {
response.articles.forEach(article => {
this.setState({
newsItems: [...this.state.newsItems, article]
});
});
});
};
render() {
let Article = null;
Article = (
<div>
{this.state.newsItems.map((news, index) => {
return (
<NewsComponent
key={index}
title={news.title}
url={news.url}
description={news.description}
author={news.author}
publish={news.publishedAt}
image={news.urlToImage}
/>
);
})}
</div>
);
return (
<div className="App">
{Article}
<button onClick={this.toogleDisplayHandler}>
{this.state.displayStatus === true
? "Hide Article"
: "Display Articles"}
</button>
</div>
);
}
}
export default News;
1) You can add a check either your state has the data which you want to show on screen to render the view.
2) Please use ComponentDidMount React life cycle function to fetch data from an external source and update this data in the state. In the Render method, it will keep calling it recursively.

how to iterate through json data from api in react.js

I just started learning React and am trying to loop through an array of JSON data. However, I am getting some syntax errors. I'm trying to use the array.map function, but it's not working properly, and I'm not exactly sure how to implement it to make it display each element in the JSON array instead of just one. Any help is greatly appreciated - thanks!
import React, { Component } from 'react';
import axios from "axios";
import './App.css';
import UserForm from "./components/UserForm.js";
class App extends Component {
state = {
name: "",
stars: "",
icon: "",
trails: [], isLoaded: false
}
getUser = (e) => {
e.preventDefault();
const address = e.target.elements.address.value;
if (address) {
axios.get(`https://www.hikingproject.com/data/get-trails?lat=40.0274&lon=-105.2519&maxDistance=10&key=200279581-dd891420fa2c470dbb683b34e017062a`)
.then((res) => {
console.log(res);
const trailList = res.data.trails.map((trail) => {
console.log(trail.name)
console.log(trail.stars)
return <div> <p>{trail.name}</p> </div>
})
this.setState({ trails: trailList, isLoaded: true });
const name = res.data.trails.name;
const stars = res.data.trails.stars;
const icon = res.data.trails.imgMedium;
this.setState({ name });
this.setState({ stars });
this.setState({ icon });
})
}
else return;
}
render() {
return (
<div>
<div className="App">
<header className="App-header">
<h1 className="App-title">HTTP Calls in React</h1>
</header>
<UserForm getUser={this.getUser} />
<div className="newmessage">
{this.state.trails.map((obj) => {
return(
<div>
<p>{obj.name}</p> >
<p> {obj.stars}</p>
</div>
);
}}
</div>
</div>
</div>
</div>
);
}
};
export default App;
A good start would be to fetch your data in the componentDidMount either with fetch or axios. Never used axios, so I am going to answer the question with fetch
Leave the constructor as it is. Then write a componentDidMount like so:
componentDidMount() {
fetch('https://www.hikingproject.com/data/get-trails?lat=40.0274&lon=-105.2519&maxDistance=10&key=200279581-dd891420fa2c470dbb683b34e017062a')
.then(res => res.json())
.then(data => this.setState({ trails: data.trails }))
.catch(e => console.log(e))
}
then in a sub-render method, such as renderData, write the following code:
renderData() {
if (!this.state.trails) {
return null;
}
return this.state.trails.map(trail => <p>{trail.name}</p>);
}
Then call {this.renderData()} in your render
render() {
return (
<div>{this.renderData()}</div>
)
}
This code has been tested on my local environment and it was working as it should.

Resources