React - Trying to render property of object, though it stays "undefined" - javascript

console.log(detailtext) shows me that the data of the object is there, the props seem to work, but I can't display the properties of the object.
Why?
It's a very simple component:
import React from "react";
import { Link } from "react-router-dom";
class LibraryTextDetails extends React.Component {
render() {
const detailtext = this.props.detailview || {};
console.log("THIS IS THE DETAILTEXT");
console.log(detailtext);
const detailviewIds = detailtext.id;
console.log("THIS IS THE DETAILVIEW ID");
console.log(detailviewIds);
return (
<div>
<div className="covercard">
<img
src={detailtext.lead_image_url}
width={157}
className="coverdetailimage"
/>
</div>
<div className="titledetailcard">{detailtext.title}</div>
<div className="authordetailcard">{detailtext.author}</div>
</div>
);
}
}
export default LibraryTextDetails;
Here is the console.log:

You are passing in an array as detailview to your component. The data you see in your console is the data of the first object in the array. Make sure you render detailview[0], and it will work.
Example
class LibraryTextDetails extends React.Component {
render() {
const { detailview } = this.props;
const detailtext = (detailview && detailview[0]) || {};
return (
<div>
<div className="covercard">
<img
src={detailtext.lead_image_url}
width={157}
className="coverdetailimage"
/>
<div className="titledetailcard">{detailtext.title}</div>
<div className="authordetailcard">{detailtext.author}</div>
</div>
</div>
);
}
}

you are missing important thing
detailtext is array of objects
when you see in log [{ ... }] it means it's Array with one object
log also displays first object in array below
so correct use would be
detailtext[0].lead_image_url
detailtext[0].title
and similar or use proxy variable
or fix the way you send data to this component not to be array but just an object (first in array)

Related

How to append a dynamic HTML element to React component using JSX?

I'm new to Reactjs. I'm creating an App with a Survey creation like Google Forms. My component has a button to create a new Div with some HTML elements to create a survey question. To do that, on the button click function, I'm creating a JSX element and push it to an array. And then, I set the array inside the render function to display what inside it.
The problem is, Even though the Array is updating, the dynamic HTML part can not be seen on the page. The page is just empty except the button. How can I fix this?
Component:
import '../../styles/css/surveycreation.css';
import React, { Component } from 'react';
let questionId = 0;
class SurveyCreation extends Component {
constructor(props) {
super(props);
this.questionsList = [];
this.state = {
}
}
addQuestion = (e) => {
e.preventDefault();
questionId = questionId + 1;
this.questionsList.push(
<div key={questionId}>
question block
</div>
)
}
render() {
return (
<div>
<button onClick={e => this.addQuestion(e)}>Add Question</button>
<div>
{this.questionsList}
</div>
</div>
);
}
};
export default SurveyCreation;
The only way a react component knows to rerender is by setting state. So you should have an array in your state for the questions, and then render based on that array. In the array you want to keep data, not JSX elements. The elements get created when rendering.
constructor(props) {
super(props);
this.state = {
questions: [],
}
}
addQuestion = () => {
setState(prev => ({
// add some object that has the info needed for rendernig a question.
// Don't add jsx to the array
questions: [...prev.questions, { questionId: prev.questions.length }];
})
}
render() {
return (
<div>
<button onClick={e => this.addQuestion(e)}>Add Question</button>
<div>
{this.state.questions.map(question => (
<div key={question.questionId}>
</div>
)}
</div>
</div>
);
}
I think you're component is not re-rendering after you fill the array of elements.
Try adding the questionsList to the component's state and modify your addQuestion method so that it creates a new array, finally call setState with the new array.
You need to map your this.questionsList variable.
You can save the 'question string' in the array and then iterate the array printing your div..
Something like this.
<div>
{this.state.questionsList.map(questionString, i => (
<div key={i}>
{questionString}
</div>
)}
</div>

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: '' }}};

Getting info from two different end points into individual list items

I have data from my parent App component that is passing two different states (state.names and state.ages - both are arrays) into my card component. In my card component I want to use this data to render individual card list items (that feature each character's name and age). I haven't been able to figure out how to combine these two arrays. I've mapped both, but I'd like to match up the names with their corresponding ages.
card component -
import React, { Component } from "react";
class Card extends Component {
render() {
const names = this.props.data.names.map(name => {
return (
<div key={name.id}>
<div>{name.first_name}</div>
//I want the ages to go right here
</p>
</div>
);
});
const ages = this.props.data.ages.map(age => {
return (
<div>
<span>{age.number}</span>
</div>
);
});
return(
<div className="card">
{people}
</div>
)
}
}
export default Card;
Instead of having two arrays for name and age you should have a single array of person objects with fields for name and age in your state. This can be mapped in the render method of your Card component as follows:
const people= this.props.data.persons.map(person => {
return (
<div key={person.id}>
<div>{person.first_name}</div>
<div>{person.age} </div>
</p>
</div>
);
});
You can add a second parameter to Array.map which is an index of the Array. So, assuming that the names and age are in the same order, it looks like this should work:
import React, {Component} from "react";
class Card extends Component {
render() {
const ages = this.props.data.ages
const names = this.props.data.names.map((name, idx) => {
return (
<div key={name.id}>
<div>{name.first_name}</div>
<div>
<span>{ages[idx].number}</span>
</div>
</div>
);
});
return (
<div className="card">
{people}
</div>
)
}
}
export default Card;
But, it must be pointed out that it would be better to keep the arrays together within objects, rather than arbitrarily ordered in two separate arrays. (e.g. Should you ever sort the age array, the names and ages would no longer correspond).
I usually convert the lists into objects or Map when I get the data from the API, so, in the store it's not an array.
This way you can link one array to another using some keys.
In the following example I'll use objects, because it's cleaner for presentation, but I personally prefer Map instead of pure objects, because on map you can do forEach, you can keep the original ordering. Object.keys(obj) gives you an array with keys of obj instance, ordered alphabetically.
store = {
users: {
'aa1001': {
id: 'aa1001',
first_name: 'Frank'
},
'aa1002': {
id: 'aa1002',
first_name: 'John'
}
},
ages: {
'aa1001': {
userId: 'aa1001',
age: 25
},
'aa1002': {
userId: 'aa1002',
age: 30
}
}
}
import React, { Component } from "react";
class Card extends Component {
render() {
const {names, ages} = this.props.data;
const people = Object.keys(names).map(nameKey => {
return (
<div key={names[nameKey].id}>
<div>{name[nameKey].first_name}</div>
<div>ages[nameKey].age</div>
</p>
</div>
);
});
return(
<div className="card">
{people}
</div>
)
}
}
export default Card;
I usually do transform the response with a custom function, but a good tool that you can use is normalizr

Annoying React Error... "TypeError: Cannot read property 'My_Items' of null"

I have a child component I need to pass an object to via props and find an index value inside an array within that object. If you look at the code below it is fairly straightforward.
I get this error:
TypeError: Cannot read property 'My_Items' of null
I've tried initializing the 'My_Items' array a bunch of different ways but I still get the error.
How can I access those values in that array from the IndexComponent and avoid this error?
import React, {Component} from 'react';
import {connect} from "react-redux";
function IndexComponent(props) {
const {
typeName,
myObject
} = props;
const getAtIndex = (type) => { //this function retrieves the index of the desired item inside the My_Items array, inside the larger object
let index;
myObject.My_Items.map(item => {
if(item.title === type) {
index = myObject.My_Items.indexOf(item);
}
})
return index;
}
return (
<div>
My Desired Index Value: {getAtIndex(typeName)} <br/>
</div>
)
}
class MyComponent extends Component {
render() {
const {
typeName,
myObject
} = this.props;
return (
<div className="row">
<div className="col-xl-5">
<IndexComponent typeName={typeName} myObject={myObject} />
</div>
</div>
)}
export default connect(
(store) => {
return {
typeName: store.data.typeName,
myObject: store.data.myObject
};
}
)(MyComponent);
Looks like the MyComponent (and IndexComponent) rendering starts before store.data.myObject is initialized. I would recommend to
1) check the initial redux state; there should be something like
data: {
myObject: {
My_Items: [],
// ...
},
// ...
}
2) protect IndexComponent render in a way like
return myObject && myObject.My_Items && myObject.My_Items.length ? (
<div>
My Desired Index Value: {getAtIndex(typeName)} <br/>
</div>
) : ( null )
or with p.(1)
return myObject.My_Items.length ? (
<div>
My Desired Index Value: {getAtIndex(typeName)} <br/>
</div>
) : ( null )

unsure why state is changing

I have a button in a child component for debugging purposes that prints the current state in its parent component. When it prints, that is how I want the state to be. When I hit another button in the same child component, the state changes in the parent, by removing a few properties.
Notice how the Id and SEO_CSEO_Survey_Questions__r properties are now missing.
Child Component
import React, { Component } from 'react';
import { Link } from 'react-router';
class Index extends Component {
constructor(props){
super(props)
this.state = {
selected: 'created'
}
this.updatePreview = this.updatePreview.bind(this);
this.renderEditArea = this.renderEditArea.bind(this);
}
isActive(value) {
return 'slds-tabs--default__item '+((value===this.state.selected) ?'slds-active':'');
}
updatePreview(e){
const updatedPreview = {
...this.props.selectedPreview,
[e.target.name]: e.target.value
};
this.props.update(updatedPreview)
}
// determines which type of edit area should display
// survey settings or question
renderEditArea() {
let selected = this.props.selectedPreview;
let hasNameKey = "Name" in selected;
if(hasNameKey){
return (
<div>
<input
onChange={(e) => this.updatePreview(e)}
name="Name"
type="text"
className="slds-input"
value={selected.Name ? selected.Name : ''}
/>
<input
onChange={(e) => this.updatePreview(e)}
name="SEO__Welcome_Text__c"
type="text"
className="slds-input"
value={selected.SEO__Welcome_Text__c ? selected.SEO__Welcome_Text__c : ''}
/>
</div>
)
}else {
return (
<input
onChange={(e) => this.updatePreview(e)}
name="SEO__Question__c"
type="text"
className="slds-input"
value={selected.SEO__Question__c ? selected.SEO__Question__c : ''}
/>
)
}
}
render() {
return (
<div className="slds-size--1-of-1 slds-medium-size--4-of-5 slds-large-size--4-of-5">
<div className="slds-tabs--default">
<h2 className="slds-text-heading--small slds-p-top--x-small" style={{position: "absolute"}}>
<button onClick={this.props.addQuestion} className="slds-button slds-button--icon slds-p-left--xx-small" title="add sur">
<svg className="slds-button__icon slds-button__icon--medium" aria-hidden="true">
<use xlinkHref={addIcon}></use>
</svg>
<span className="slds-assistive-text">Add Question</span>
</button>
</h2>
<ul className="slds-tabs--default__nav" style={{justifyContent: "flex-end"}}>
<Link to="/"className="slds-button slds-button--neutral">Back</Link>
<button onClick={this.props.save} className="slds-button slds-button--brand">Save</button>
<button onClick={this.props.currentState} className="slds-button slds-button--brand">Current State</button>
</ul>
</div>
<div className="slds-grid slds-wrap slds-grid--pull-padded">
{this.renderEditArea()}
</div>
</div>
);
}
}
export default Index;
Parent
import React, { Component } from 'react';
import { getQuestions, addQuestion, deleteQuestion, newSurveyQuestions, updateSurveyQuestions, EMPTY_SURVEY } from './helpers';
import SideBar from './survey/SideBar';
import MainArea from './survey/Index';
class Survey extends Component {
constructor(props) {
super(props)
this.state = {
survey: [],
selectedPreview: []
}
this.componentWillMount = this.componentWillMount.bind(this);
this.save = this.save.bind(this);
this.setSelectedPreview = this.setSelectedPreview.bind(this);
this.currentState = this.currentState.bind(this);
}
// if the url is `/survey/new`, create an empty survey
// to save for later.
// else if there is an id in the url, load the survey and questions
componentWillMount(){
if(this.props.pathname === "/survey/new") {
this.setState({
survey: EMPTY_SURVEY,
selectedPreview: EMPTY_SURVEY[0]
})
} else if (this.props.params.surveyId){
getQuestions(this.props.params.surveyId).then(survey => {
// 'survey' contains all the questions
this.setState({
survey,
selectedPreview: survey[0]
});
});
}
}
currentState() {
console.log('clicking Current State');
console.log(this.state.survey[0]);
}
// saves a new survey with associated newly created questions
// or saves an existing survey with associated questions
save() {
console.log('clicking Save');
console.log(this.state.survey[0]);
// if the url is set to survey/new
// save new survey with associated newly created questions
if(this.props.pathname === "/survey/new") {
newSurveyQuestions(this.state.survey).then( id => {
this.context.router.transitionTo(`/survey/id/${id}`);
})
// else update survey and questions
} else {
updateSurveyQuestions(this.state.survey);
}
}
// sets selectedPreview for the entire component and
// its children
setSelectedPreview(selectedPreview) {
this.setState({selectedPreview});
}
render() {
return (
<div className="slds-grid slds-wrap slds-grid--pull-padded">
<SideBar
survey={this.state.survey}
setSelectedPreview={this.setSelectedPreview}
deleteQuestion={this.deleteQuestion}
/>
<MainArea
addQuestion={this.addQuestion}
selectedPreview={this.state.selectedPreview}
update={this.update}
save={this.save}
currentState={this.currentState}
/>
</div>
);
}
}
Survey.contextTypes = {
router: React.PropTypes.object
}
export default Survey;
help function
export function updateSurveyQuestions(survey) {
// create proper url for update request
const requestURL = `${URL + survey[0].attributes.url}`;
// save questions for later update requests
const questions = survey[0].SEO__CSEO_Survey_Questions__r.records;
let s = [...survey];
// remove properties for proper body format
delete s[0].Id;
delete s[0].SEO__CSEO_Survey_Questions__r;
delete s[0].attributes;
axios.patch(requestURL, s[0], API_TOKEN);
questions.forEach( question => {
// save request url for later
let requestURL = `${URL + question.attributes.url }`;
// remove properites for proper body format
delete question.attributes;
delete question.Id;
axios.patch(requestURL, question, API_TOKEN);
})
}
When I removed all the code in save(), except for the console.log's, it prints as expected.
tl;dr: Your code is working fine. What you are seeing is a consequence of how objects and the console work. Do
console.log(this.state.survey[0].Id);
instead to see that the property does actually exist.
See also Is Chrome's JavaScript console lazy about evaluating arrays?
When I removed all the code in save(), except for the console.log's, it prints as expected.
That seems to suggest that that code is changing the object. E.g. updateSurveyQuestions or newSurveyQuestions might remove these properties.
You have to keep in mind that the output you see in the console is computed at the moment you expand the properties, not at the moment console.log is called. In fact, the little i icon next to Object tells you that. When you hover over it says:
Value below was evaluated just now
Here is a simplified example of what you are experiencing: Expand the object and notice that it is empty even though we removed it after calling console.dir (open your browser's console):
var obj = {foo: 42};
console.dir(obj);
delete obj.foo;
Ideally updateSurveyQuestions or newSurveyQuestions would not mutate the object directly, but rather clone it, make the necessary changes to the clone and then update the components state (if desired). E.g. a simple way to clone the object is via Object.assign:
var copy = Object.assign({}, this.state.survey[0]);

Categories

Resources