I get an infinite loop only when I render a child component - ReactJs - javascript

Okay so this is driving me crazy ! Had to restart coding the project from scratch to pinpoint where the problem is.
Basically I'm trying to practice React by building a web app where I can share spotify songs. So here's my Component tree (only the important components: App.js -> [Navbar, Posts] -> then inside Posts i have a list of Post components. Here are the codes:
import React, { Component } from 'react';
import './App.css';
import {BrowserRouter} from 'react-router-dom';
import Navbar from './components/Navigation/Navbar';
import Posts from './containers/Posts/Posts';
class App extends Component {
render() {
return (
<BrowserRouter>
<div className="App">
<Navbar />
<Posts />
</div>
</BrowserRouter>
);
}
}
export default App;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
import React, { Component } from 'react';
import Post from './Post/Post';
import $ from 'jquery';
class Posts extends Component {
state = {
posts: null,
// addingNewPost: false
}
componentDidMount() {
$.ajax({
url: 'https://music-blog-app.firebaseio.com/users/user/posts.json',
success: (response) => {
//console.log(response); // object of objects
// converting to array of objects
const responseArray = Object.keys(response).map(i => response[i]);
//console.log(responseArray);
this.setState({
posts: responseArray
})
// console.log(this.state.posts);
}
//error
});
}
// addingNewPostHandler = () => {
// this.setState({addingNewPost: true});
// }
// cancelNewPostHandler = () => {
// this.setState({addingNewPost: false});
// }
sharedNewPostHandler = (caption, embedSrcLink) => {
var newPostToAdd = {
caption: caption,
embedSrcLink: embedSrcLink
}
var postsToUpdate = this.state.posts.slice();
postsToUpdate.push(newPostToAdd);
// $.ajax({
// type: 'POST',
// url: 'https://music-blog-app.firebaseio.com/users/user/posts.json',
// success: (response) => {
// console.log(response);
// this.setState(prevState =>({
// addingNewPost: false,
// posts: [...this.state.posts, newPostToAdd]
// }));
// }
// // error
// });
}
render() {
var postsToRender = <p>Nothing here</p>
console.log(this.state.posts);
if(this.state.posts) {
var myPosts = this.state.posts.slice();
}
console.log(myPosts);
let render;
if(myPosts) {
render = (myPosts.map((post, index) => { return <p>IF I REPLACE THIS BY RENDERING POST component, I get an infinite loop</p>}))
} else {
render = <p>still waiting...</p>
}
return (
<div className="container posts-container">
{/* <p>jsdhfjhd</p>
{myPosts ? (myPosts.map((post, index) => {
// console.log(post)
return <Post key={post} caption={this.state.posts[index].caption} embedSrcLink={this.state.posts[index].embedSrcLink} />
})) : <p>still waiting...</p>} */}
{render}
</div>
);
}
}
export default Posts;
import React, { Component } from 'react';
import './Post.css';
import PosterProfile from '../../../components/PosterProfile/PosterProfile';
const post = (props) => (
<div className="post">
<PosterProfile />
<div className="card" style={{width: '18rem'}}>
<div className="card-body">
<h5 className="card-caption">{props.caption}</h5>
<div className="embed-iframe">
<iframe title="embed" src={props.embedSrcLink} width="300" height="380" frameBorder="0" allowtransparency="true" allow="encrypted-media"></iframe>
</div>
</div>
<div className="card-footer">
Like
Comment
Repost
</div>
</div>
</div>
)
export default post;
Here is the problem !! So this piece of code inside the render method of Posts:
render = (myPosts.map((post, index) => { return <p>IF I REPLACE THIS BY RENDERING POST component, I get an infinite loop</p>}))
AS SOON AS I replace it with
render = (myPosts.map((post, index) => {
return <Post key={post} caption={this.state.posts[index].caption} embedSrcLink={this.state.posts[index].embedSrcLink}
}))
I am getting the posts from a firebase database by the way.
Please help ! Thank you in advance :)

Rendering a list/array in react requires you to add a key to each item. From the docs:
Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity
https://reactjs.org/docs/lists-and-keys.html#keys
I believe what is happening in your snippets, is that you're assigning a key as an object instead of a string. This would definitely cause unexpected behavior or errors.

Related

React child callback not being executed after being passed down twice

I am working on the following project https://github.com/codyc4321/react-udemy-course section 11 the videos app. The udemy course is found at https://www.udemy.com/course/react-redux/learn/lecture/12531374#overview.
The instructor is passing a callback down to multiple children and calling it in the lowest videoItem and the code is supposed to console log something out. I have no console log in my browser even though I've copied the code as written and double checked for spelling errors.
At the main level is App.js:
import React from 'react';
import youtube from '../apis/youtube';
import SearchBar from './SearchBar';
import VideoList from './VideoList';
class App extends React.Component {
state = {videos: [], selectedVideo: null};
onTermSubmit = async term => {
const response = await youtube.get('/search', {
params: {
q: term
}
});
// console.log(response.data.items);
this.setState({videos: response.data.items});
};
onVideoSelect = video => {
console.log('from the app', video);
}
render() {
return (
<div className="ui container">
<SearchBar onFormSubmit={this.onTermSubmit} />
<VideoList
onVideoSelect={this.onVideoSelect}
videos={this.state.videos} />
</div>
)
}
}
export default App;
videoList.js
import React from 'react';
import VideoItem from './VideoItem';
const VideoList = ({videos, onVideoSelect}) => {
const rendered_list = videos.map(video => {
return <VideoItem onVideoSelect={onVideoSelect} video={video} />
});
return <div className="ui relaxed divided list">{rendered_list}</div>;
};
export default VideoList;
the videoItem.js
import React from 'react';
import './VideoItem.css';
const VideoItem = ({video, onVideoSelect}) => {
return (
<div onClick={() => onVideoSelect(video)} className="item video-item">
<img
src={video.snippet.thumbnails.medium.url}
className="ui image"
/>
<div className="content">
<div className="header">{video.snippet.title}</div>
</div>
</div>
);
}
export default VideoItem;
The code that isn't running is
onVideoSelect = video => {
console.log('from the app', video);
}
My guess is that it has something to do with a key prop not being present in the map - I'm not super well versed with class components but I can't find anything else funky so maybe try adding a unique key prop in the map.
When rendering components through a map react needs help with assigning unique identifiers to keep track of re-renders etc for performance, that also applies to knowing which specific instance called a class method.
If you don't have a unique ID in the video prop you can use an index in a pinch, although ill advised, it can be found as the second parameter in the map function. The reason it's ill advised to use an index is if there are multiple children with the same index in the same rendering context, obviously the key parameter could be confused.
Okay-ish:
const rendered_list = videos.map((video, index) => {
return <VideoItem key={index} onVideoSelect={onVideoSelect} video={video} />});
Better:
const rendered_list = videos.map((video, index) => {
return <VideoItem key={video.id} onVideoSelect={onVideoSelect} video={video} />});

React: Persisting State Using Local Storage

I am coding an app in which there is a collection of reviews and a person can respond to a review, but each review can only have one response. So far, I am doing this by rendering a ReviewResponseBox component in my ReviewCardDetails component and passing the review_id as props.
I have implemented the logic so that once there is one ReviewResponse, the form to write another will no longer appear. However, before I was initializing the state in this component with an empty array, so when I refreshed my page the response went away and the form came back up. (This is now commented out)
I am trying to resolve this by persisting my state using React LocalStorage but am having trouble writing my method to do this. Here is what I have so far:
Component that renders ReviewResponseBox and passes review_id as props:
import React from "react";
import './Review.css';
import { useLocation } from "react-router-dom";
import StarRatings from "react-star-ratings";
import ReviewResponseBox from "../ReviewResponse/ReviewResponseBox";
const ReviewCardDetails = () => {
const location = useLocation();
const { review } = location?.state; // ? - optional chaining
console.log("history location details: ", location);
return (
<div key={review.id} className="card-deck">
<div className="card">
<div>
<div className='card-container'>
<h4 className="card-title">{review.place}</h4>
<StarRatings
rating={review.rating}
starRatedColor="gold"
starDimension="20px"
/>
<div className="card-body">{review.content}</div>
<div className="card-footer">
{review.author} - {review.published_at}
</div>
</div>
</div>
</div>
<br></br>
<ReviewResponseBox review_id={review.id}/>
</div>
);
};
export default ReviewCardDetails;
component that I want to keep track of the state so that it can render the form or response:
import React from 'react';
import ReviewResponse from './ReviewResponse';
import ReviewResponseForm from './ReviewResponseForm';
import { reactLocalStorage } from "reactjs-localstorage";
class ReviewResponseBox extends React.Component {
// constructor() {
// super()
// this.state = {
// reviewResponses: []
// };
// }
fetchResponses = () => {
let reviewResponses = [];
localStorage.setResponses
reviewResponses.push(reviewResponse);
}
render () {
const reviewResponses = this.getResponses();
const reviewResponseNodes = <div className="reviewResponse-list">{reviewResponses}</div>;
return(
<div className="reviewResponse-box">
{reviewResponses.length
? (
<>
{reviewResponseNodes}
</>
)
: (
<ReviewResponseForm addResponse={this.addResponse.bind(this)}/>
)}
</div>
);
}
addResponse(review_id, author, body) {
const reviewResponse = {
review_id,
author,
body
};
this.setState({ reviewResponses: this.state.reviewResponses.concat([reviewResponse]) }); // *new array references help React stay fast, so concat works better than push here.
}
getResponses() {
return this.state.reviewResponses.map((reviewResponse) => {
return (
<ReviewResponse
author={reviewResponse.author}
body={reviewResponse.body}
review_id={this.state.review_id} />
);
});
}
}
export default ReviewResponseBox;
Any guidance would be appreciated
You would persist the responses to localStorage when they are updated in state using the componentDidUpdate lifecycle method. Use the componentDidMount lifecycle method to read in the localStorage value and set the local component state, or since reading from localStorage is synchronous directly set the initial state.
I don't think you need a separate package to handle this either, you can use the localStorage API easily.
import React from "react";
import ReviewResponse from "./ReviewResponse";
import ReviewResponseForm from "./ReviewResponseForm";
class ReviewResponseBox extends React.Component {
state = {
reviewResponses: JSON.parse(localStorage.getItem(`reviewResponses-${this.props.review_id}`)) || []
};
storageKey = () => `reviewResponses-${this.props.review_id}`;
componentDidUpdate(prevProps, prevState) {
if (prevState.reviewResponses !== this.state.reviewResponses) {
localStorage.setItem(
`reviewResponses-${this.props.review_id}`,
JSON.stringify(this.state.reviewResponses)
);
}
}
render() {
const reviewResponses = this.getResponses();
const reviewResponseNodes = (
<div className="reviewResponse-list">{reviewResponses}</div>
);
return (
<div className="reviewResponse-box">
{reviewResponses.length ? (
<>{reviewResponseNodes}</>
) : (
<ReviewResponseForm addResponse={this.addResponse.bind(this)} />
)}
</div>
);
}
addResponse(review_id, author, body) {
const reviewResponse = {
review_id,
author,
body
};
this.setState({
reviewResponses: this.state.reviewResponses.concat([reviewResponse])
}); // *new array references help React stay fast, so concat works better than push here.
}
getResponses() {
return this.state.reviewResponses.map((reviewResponse) => {
return (
<ReviewResponse
author={reviewResponse.author}
body={reviewResponse.body}
review_id={this.state.review_id}
/>
);
});
}
}

TypeError: Cannot read property of undefined using React

I'm new in React and I'm doing a little app with PokeAPI. I have a component called PokemonDetail in which I want to show the details of a pokemon, but the app throws me the next error
TypeError: Cannot read property 'front_default' of undefined
my component looks like this:
import React from "react";
const PokemonDetail = ({ pokemon }) => {
return (
<div>
<div className="text-center">{pokemon.name}</div>
<img src={pokemon.sprites.front_default} alt={pokemon.name} />
{pokemon.id}
</div>
);
};
export default PokemonDetail;
And the App component from which the PokemonDetail recive the prop of pokemon looks like this:
import React from "react";
import PokeAPI from "../apis/PokeAPI";
import SearchBar from "./SearchBar";
import PokemonDetail from "./PokemonDetail";
class App extends React.Component {
state = { pokemon: '' };
onTermSubmit = async term => {
try {
const response = await PokeAPI.get(`pokemon/${term}`);
this.setState({ pokemon: response.data });
console.log(response);
} catch (error) {
console.log("No existe");
}
};
render() {
return (
<div className="container">
<div className="row mt-3">
<div className="col">
<SearchBar onFormSubmit={this.onTermSubmit} />
</div>
</div>
<div className="row mt-3">
<div className="col-9" />
<div className="col-3">
<PokemonDetail pokemon={this.state.pokemon} />
</div>
</div>
</div>
);
}
}
export default App;
I don't understand why it throws me this error because only throws it with this and other properties of the json. With the name property works and wait until I send it some props, same with the id but no with the front_default property, which is a url of a image.
Because ajax is slower than react rendering, you can use a loading component before you get the data.
const PokemonDetail = ({ pokemon }) => {
if(pokemon.sprites == undefined){
return(
<div>
Loading...
</div>
);
}
return (
<div>
<div className="text-center">{pokemon.name}</div>
<img src={pokemon.sprites.front_default} alt={pokemon.name} />
{pokemon.id}
</div>
);
};
Very likely just an AJAX issue, your component renders before it has time to complete your request to the API. Try adding an additional check before rendering the image.
import React from "react";
const PokemonDetail = ({ pokemon }) => {
return (
<div>
<div className="text-center">{pokemon.name}</div>
{pokemon.sprites ? (
<img src={pokemon.sprites.front_default} alt={pokemon.name} />
) : (
null
)
}
{pokemon.id}
</div>
);
};
export default PokemonDetail;
#ZHAOXIANLONG gave you the best solution (use a loading component until you receive data), but, if you do not use a loading component, you can use the get method from lodash library [1] in order to avoid a possible error.
import React from "react";
import _ from 'lodash';
const PokemonDetail = ({ pokemon }) => {
const front_default = _.get(pokemon, 'sprites.front_default', 'DEFAULT_VALUE');
const name = _.get(pokemon, 'name', 'DEFAULT_VALUE');
return (
<div>
<div className="text-center">{pokemon.name}</div>
<img src={pokemon.sprites.front_default} alt={pokemon.name} />
{pokemon.id}
</div>
);
};
export default PokemonDetail;
where the third parameter ('DEFAULT_VALUE') is a default value that will be used if the lodash can not retrieve a value for your query.
PS: I advise you to use lodash even in #ZHAOXIANLONG solution if you know that your API Server can be changed.
[1] https://lodash.com/docs/4.17.11#get
The initial state is { pokemon: '' }; pokemon is an empty string. PokemonDetail is referring to pokemon.sprites.front_default, but pokemon is initially a string and a string does not have a field called sprites.
If you are expecting pokemon to eventually become an object, you could initialize it to something that looks like an object:
state = { pokemon: { sprites: {front_default: '' }}};

TypeError: this.state.userInfo.map is not a function

Sorry, I'm kinda new to react ,why I'm not being able to map through the data.
I have tried a different couple of things but nothing has helped.
Maybe the reason is that it's an object.
Can any one help?
import React, { Component } from "react";
import axios from "axios";
import "./Profile.css";
import ProfileCard from "../ProfileCard/ProfileCard";
class Profile extends Component {
state = {
userInfo: {}
};
componentDidMount() {
const { id } = this.props.match.params;
axios
.get(`/api/user/info/${id}`)
.then(
response => this.setState({ userInfo: { ...response.data, id } }),
() => console.log(this.state.userInfo)
);
}
render() {
let userInfoList= this.state.userInfo.map((elem,i)=>{
return(
<div> name={elem.name}
id={elem.id}</div>
)
})
console.log(this.state.userInfo);
return (
<div>
{/* <p>{this.state.userInfo}</p> */}
{/* <div >{userInfoList}</div>
<ProfileCard profilePic={this.state.userInfo} /> */}
</div>
);
}
}
export default Profile;
I think I understand what youre trying to do.
First you should change userInfo to an empty array instead of an empty object as others have stated.
Next since you are making an async api call you should use a ternary expression in your render method, because currently React will just render the empty object without waiting for the api call to complete. I would get rid of the userInfoList variable and refactor your code to the following:
RenderProfile = (props) => (
<div>
{props.elem.name}
</div>
)
{ this.state.userInfo
? this.state.userInfo.map(elem => < this.RenderProfile id={elem.id} elem={elem} /> )
: null
}
Let me know if it worked for you.

When does mapStateToProps occur in the component life cycle

I have a list of items in table. I want to be able to click on a details link on any item and go to a new page where I see the details of the item. Below is my code.
import React from 'react';
import { connect } from 'react-redux';
import toastr from 'toastr';
import { Link } from 'react-router';
import { bindActionCreators } from 'redux';
import * as documentActions from '../../actions/documentActions';
class DocumentDetailsPage extends React.Component {
constructor(props) {
super(props);
this.deleteDoc = this.deleteDoc.bind(this);
}
deleteDoc(id) {
this.props.actions.deleteDocument(id)
.then(res => toastr.success('Document deleted successfully!'));
}
render() {
const { document } = this.props;
return (
<div className="col s12">
<div className="card qBox">
<div className="card-content white-text">
<span className="card-title">{document.title}</span>
<p
dangerouslySetInnerHTML={{ __html: document.content }}
className="document-content"></p>
<br />
<p>Access Type:
<span>{(document.access).toUpperCase()}</span></p><br />
<div>
Published Date :
<p>{(document.createdAt) ?
document.createdAt.split('T')[0] : ''}</p>
<p id="owner">Author:
{document.owner.firstName} {document.owner.lastName}</p>
</div>
</div>
<div className="card-action">
<Link to="/">back</Link>
{this.props.auth.user.userId === document.ownerId &&
<div className="right">
<Link to={`/document/${document.id}`}>Edit</Link>
<Link to="/" onClick={this.deleteDoc()}> Delete </Link>
</div>
}
</div>
</div>
</div>
);
}
}
DocumentDetailsPage.propTypes = {
document: React.PropTypes.object.isRequired,
actions: React.PropTypes.object.isRequired,
auth: React.PropTypes.object.isRequired,
};
function getDocumentById(documents, id) {
const document = documents.filter(item => item.id === id);
if (document) return document[0];
return null;
}
function mapStateToProps(state, ownProps) {
const documentId = ownProps.params.id; // from the path `/document/:id`
let document;
if (documentId && state.documents.length > 0) {
document = getDocumentById(state.documents, parseInt(documentId, 10));
}
return {
document,
auth: state.auth,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(documentActions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(DocumentDetailsPage);
When I click on the details link, it should take me to this page with route document/:id where I can read the id from the route and fetch the corresponding document from state and render in the component. But when i go to this page, it tells me that document is undefined. Any reason for this? or is there a better way to get the param id from the route than using mapStateToProps?
You have two problems here:
1) Your deleteDoc class method expects an id argument which is passed to your action creator. Because you already have the full document as a prop, get it from the object instead of receiving it:
deleteDoc() {
const { actions, document } = this.props
actions.deleteDocument(document.id)
.then(res => toastr.success('Document deleted successfully!'));
}
2) You're CALLING the deleteDoc function in your onClick handler!
// Wrong
<Link to="/" onClick={this.deleteDoc()}> Delete </Link>
Remove the parens and simply set a reference to your function as the onClick prop, not the result from an actual call. :)
// Right
<Link to="/" onClick={this.deleteDoc}> Delete </Link>

Categories

Resources