React state is array of objects which are react elements - javascript

I'm running into an issue right now trying to render a list using react, where I'm saving my react elements into the state, but the problem I'm getting is that the console outputs this:
Uncaught Error: Objects are not valid as a React child (found: object with keys {}). If you meant to render a collection of children, use an array instead.
Here is what the state looks like which causes the error:
export default class UserData extends Component {
constructor() {
super();
this.state = {
resultsItems: {}
}
};
componentDidMount() {
fetch(url)
.then(results => {
return results.json();
}).then(data => {
console.log(data.items);
let items = data.items.map((item) => {
console.log(item.title);
return (
<li>
<h2>item.title</h2>
</li>
)
});
this.setState({resultsItems: items});
console.log("state", this.state.resultsItems);
})
.catch(error => console.log(error))
};
render() {
return (
<div>
<button onClick={() => this.props.updateLoginStatus(false)}>
Logout
</button>
<div>
ID: {this.props.user}
{this.state.resultsItems}
</div>
</div>
)
}
}

By way of demonstrating the sort of thing Hamms is talking about in their comment:
class UserData extends Component {
constructor () {
super()
this.state = {
resultsItems: []
}
}
componentDidMount () {
// Simulate API response
const resultsItems = [
{ title: 'foo' },
{ title: 'bar' },
{ title: 'wombat' }
]
this.setState({ resultsItems })
}
render () {
return (
<div>
{this.state.resultsItems.map(item => <ResultsItem item={item} />)}
</div>
)
}
}
function ResultsItem ({ item }) {
return <li>{item.title}</li>
}
However, Chris' answer is correct as to the cause of the error message: the first render tries to use an empty object and not an array, which fails.

It seems like you are correctly setting an array to your state on componentDidMount, however the initial state in your constructor is an object and not an array!
So change this:
this.state = {
resultsItems: {}
}
to this:
this.state = {
resultsItems: []
}

Related

I can not collect data from my json - React

I have created a menu, with a submenu and a third child. So far I had it done simply with a json in local const data that is now commented. I need that from now on the data is collected from my json but I do not know how to do it. As it is now I get the following error: 'data' is not defined ( in my render)
class Nav extends Component {
constructor(props){
super(props)
this.state = {
navigation:[]
}
}
componentWillMount() {
fetch('json_menuFIN.php')
.then(response => response.json())
.then(data =>{
this.setState({navigation: data });
console.log( data)
})
}
render(){
const { data = null } = this.state.navigation;
if ( this.state.navigation && !this.state.navigation.length ) { // or wherever your data may be
return null;
}
return (
<Menu data={this.state.navigation}/>
)
}
}
const renderMenu = items => {
return <ul>
{ items.map(i => {
return <li>
<a href={i.link}>{ i.title }</a>
{ i.menu && renderMenu(i.menu) }
</li>
})}
</ul>
}
const Menu = ({ data }) => {
return <nav>
<h2>{ data.title }</h2>
{ renderMenu(data.menu) }
</nav>
}
I do not know what else to do to make it work with what I have. Thank you very much for the help.
Your navigation property in state has no title and menu properties, so you pass an empty array to Menu component. That's why you have an error Cannot read property 'map' of undefined. You should change your state initialization in constructor.
class Nav extends Component {
constructor(props){
super(props);
this.state = {
navigation: {//<-- change an empty array to object with a structure like a response from the server
menu: [],
title: ''
}
}
}
//...
render(){
return (
<Menu data={this.state.navigation} />
)
}
}
Don't use componentWillMount as it is deprecated and will soon disappear, the correct way is to use componentDidMount method along with a state variable and a test in your render.
this.state = {
navigation: [],
init: false
}
componentDidMount() {
fetch('json_menuFIN.php')
.then(response => response.json())
.then(data => {
this.setState({ navigation: data, init: true });
console.log( data)
})
}
Also, you cannot extract the data variable from your navigation variable in the state, navigation has been defined with your data response, so use it directly.
render() {
const { navigation, init } = this.state;
if(!init) return null
return (
<Menu data={navigation}/>
)
}
I assume that navigation is always an array, whatever you do with it.

I'm unable to change the array of an existing state

I'm trying to change one value inside a nested state.
I have a state called toDoItems that is filled with data with componentDidMount
The issue is that changing the values work and I can check that with a console.log but when I go to setState and then console.log the values again it doesn't seem like anything has changed?
This is all of the code right now
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
toDoItems: null,
currentView: "AllGroup"
};
}
componentDidMount = () => {
fetch("/data.json")
.then(items => items.json())
.then(data => {
this.setState({
toDoItems: [...data],
});
})
};
changeToDoItemValue = (givenID, givenKey, givenValue) => {
console.log(this.state.toDoItems);
let newToDoItems = [...this.state.toDoItems];
let newToDoItem = { ...newToDoItems[givenID - 1] };
newToDoItem.completedAt = givenValue;
newToDoItems[givenID - 1] = newToDoItem;
console.log(newToDoItems);
this.setState({
toDoItems: {newToDoItems},
})
console.log(this.state.toDoItems);
};
render() {
if (this.state.toDoItems) {
// console.log(this.state.toDoItems[5 - 1]);
return (
<div>
{
this.state.currentView === "AllGroup" ?
<AllGroupView changeToDoItemValue={this.changeToDoItemValue}/> :
<SpecificGroupView />
}
</div>
)
}
return (null)
};
}
class AllGroupView extends Component {
render() {
return (
<div>
<h1 onClick={() => this.props.changeToDoItemValue(1 , "123", "NOW")}>Things To Do</h1>
<ul className="custom-bullet arrow">
</ul>
</div>
)
}
}
So with my console.log I can see this happening
console.log(this.state.toDoItems);
and then with console.log(newToDoItems)
and then again with console.log(this.state.toDoitems) after setState
State update in React is asynchronous, so you should not expect updated values in the next statement itself. Instead you can try something like(logging updated state in setState callback):
this.setState({
toDoItems: {newToDoItems},// also i doubt this statement as well, shouldn't it be like: toDoItems: newToDoItems ?
},()=>{
//callback from state update
console.log(this.state.toDoItems);
})

Ajax call for each item in a map function

I have the following scenario where I need to make an ajax request to an endpoint for each item in the array while i map over it and display it on screen.
const items = [
{
id: 1,
name: "test"
},
{
id: 2,
name: "test2"
}
]
In my render() method:
render() {
return (
items.map((item) => {
<div>{item.name}</div>
// function that returns a promise
// the endpoint requires the item's id
this.apiCall(item.id).then((returnedValue) => {
return <div>{returnedValue}</div>
});
})
);
}
But running it throws this error:
Uncaught (in promise) Error: Minified React error #31; Objects are not valid as a React child (found: [object Promise]).
We can use Promise.all() like that:
return (
Promise.all(items.map((item) => {
<div>{item.name}</div>
// function that returns a promise
// the endpoint requires the item's id
this.apiCall(item.id).then((returnedValue) => {
return <div>{returnedValue}</div>
});
})
);
You cannot perform an async operation in the render method, but you could encapsulate the Api call in a child component (which you would render instead), then you could handle the call inside the child component logic, something along these lines:
constructor(props) {
apiCall(props.id).then((value) => { this.setState({value}) });
this.state = {
value: ''
}
}
render() {
return (<div>{this.state.value}</div>);
}
You need to return your JSX synchronously from the render method, so you are better of getting your API data in componentDidMount instead and setting it in the component state when the requests are done.
Example
class App extends React.Component {
state = { items: [] };
componentDidMount() {
const items = [
{
id: 1,
name: "test"
},
{
id: 2,
name: "test2"
}
];
Promise.all(
items.map(item =>
this.apiCall(item.id).then(returnedValue => {
item.returnedValue = returnedValue;
return item;
})
)
).then(items => {
this.setState({ items });
});
}
apiCall = id => {
return new Promise(resolve =>
setTimeout(() => resolve(Math.random()), 1000)
);
};
render() {
const { items } = this.state;
return (
<div>
{items.map(item => (
<div key={item.id}>
<div>{item.name}</div>
<div>{item.returnedValue}</div>
</div>
))}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<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>
<div id="root"></div>

How to Set State with arguments passed to a function in React

I'm trying to pass an array (titles) from a child component to the parent, then set the state of the parent with the array. However, when handling the change in the increaseReads() method, I cannot change the articlesRead state
You will see two console.log() statements; the first one is successfully logging the titles but the second is logging an empty array - the previous state
The Child:
export class Publication extends React.Component {
constructor() {
super();
this.state = {
items: []
};
}
componentDidMount() {
fetch(this.props.url)
.then(response => {
return response.json();
}).then(({ items })=> {
this.setState({ items });
});
}
handleClick () => {
this.props.openArticle();
}
render() {
return (
<div className='publication'>
<h4>{this.props.name}</h4>
<ul>
{this.state.items.map(item => (
<li><a href={item.link} target='_blank' onClick={this.handleClick}>{item.title}</a></li>
))}
</ul>
</div>
);
}
}
The Parent:
export class Latest extends React.Component {
constructor(props) {
super(props);
this.state = {
totalReads: 0,
articlesRead: []
};
}
handleChange = () => {
this.props.increaseTotal();
}
increaseReads(titles) {
this.setState({
totalReads: this.state.totalReads + 1,
articlesRead: titles
})
// Won't log correctly
console.log(this.state.articlesRead);
this.handleChange();
}
render() {
return (
<div className='container'>
<Publication total={(titles) => {this.increaseReads(titles)}} name='Free Code Camp' api={'https://api.rss2json.com/v1/api.json?rss_url=https%3A%2F%2Fmedium.freecodecamp.org%2Ffeed%2F'}/>
<Publication total={() => {this.increaseReads()}} name='Code Burst' api={'https://api.rss2json.com/v1/api.json?rss_url=https%3A%2F%2Fcodeburst.io%2Ffeed%2F'}/>
<Publication total={() => {this.increaseReads()}} name='JavaScript Scene' api={'https://api.rss2json.com/v1/api.json?rss_url=https%3A%2F%2Fmedium.com%2Ffeed%2Fjavascript-scene%2F'}/>
<Publication total={() => {this.increaseReads()}} name='Hacker Noon' api={'https://api.rss2json.com/v1/api.json?rss_url=https%3A%2F%2Fhackernoon.com%2Ffeed'}/>
</div>
)
}
}
I'm sure it is something small, but any help would be greatly appreciated!
The issue might be that you are expecting this.setState to be synchronous. See the documentation here.
Take a look at this CodeSandbox demo. this.setState accepts a callback as the second argument. This callback is invoked after this.setState has completed.
Notice how in the console.log output, we can see the old and new state values.

How can I use `setState` with objects nested in an array in React JS?

With this code, I am able to successfully use setState on a simple object – when I click on "Joey" the name changes to "Igor".
class Card extends React.Component {
myFunc = () => {this.props.change('Igor')};
render() {
return (
<p onClick={this.myFunc}>{this.props.name}</p>
)
}
}
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = { name: "Joey" }
}
toggle = (newname) => {
this.setState((prevState, props) => ({
name: newname
}));
}
render() {
return (
<Card change={this.toggle} name={this.state.name} />
);
}
}
But with this code, which has multiple objects nested in an array, setState is either not able to change each name to "Igor" or it must be modified in some way.
class Card extends React.Component {
myFunc = () => {this.props.change('Igor')};
render() {
return (
<p onClick={this.myFunc}>{this.props.name}</p>
)
}
}
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {
names: [
{
name: "Joey"
},
{
name: "Sally"
},
{
name: "Billy"
},
]
}
}
toggle = (newname) => {
this.setState((prevState, props) => ({
// what can I put here to change the name I click on to "Igor"
}));
}
render() {
const names = this.state.names.map((name, index) => (
<Card key={index} change={this.toggle} {...name} />
))
return (
<div>
{names}
</div>
);
}
}
Even though I know this is not how setState works, I tried to access name by passing index and then writing this.state.names[index].name: newname. No surprises here, it didn't work.
I have researched and cannot find similar questions on SO about this although I have found a lot of mentions with regards to immutability helpers. But I am still not sure if that is the way to go.
What is the best way to use setState to modify objects nested in an array?
Have modified your code and the working example can be found here.
The changes can be found here:
toggle = (index, newname) => {
this.setState((prevState, props) => ({
// Return new array, do not mutate previous state.
names: [
...prevState.names.slice(0, index),
{ name: newname },
...prevState.names.slice(index + 1),
],
}));
}
render() {
const names = this.state.names.map((name, index) => (
// Need to bind the index so callback knows which item needs to be changed.
<Card key={index} change={this.toggle.bind(this, index)} {...name} />
))
return (
<div>
{names}
</div>
);
}
The idea is that you need to pass the index into the callback function via .bind, and return a new state array with the modified name. You need to pass the index so that the component knows which object to change the name to newname.
I would use this for the toggle method:
toggle = (nameYouWantChanged, nameYouWantItChangedTo) => {
this.setState({
names: this.state.names.map(obj =>
obj.name === nameYouWantChanged
? { name: nameYouWantItChangedTo }
: obj
)
})
}

Categories