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.
Related
I'm not sure what I'm doing wrong or what I'm missing but I can't iterate over my array of objects when I do my get request. I get the results on my console but they don't appear on the page unless I call one individual item. It only happens when I'm using React, I have also tried with JSON placeholder fake APIs and get the same result. Is there anything in React that stops this iteration to be executed? Thank you so much for your help!
import React, { Component } from 'react';
class UserList extends Component {
constructor(props) {
super(props);
this.state = {
users: [],
isLoaded: false,
};
}
componentDidMount() {
this.loadData();
}
loadData = async () => {
const response = await fetch('http://dev.pubmate.io/pubmate/api/0.1/user/all');
if (response) {
const allusers = await response.json();
console.log(response);
console.log(allusers);
console.log(allusers[0].email);
this.setState({
isLoaded: true,
users: [...allusers],
});
}
};
render() {
let { isLoaded, users } = this.state;
if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<div className="userlist">
Data is been loaded
<ul>
{users.map((user) => {
<li key={user.id}>{user.title}</li>;
})}
</ul>
</div>
);
}
}
}
export default UserList;
You were quite close, but just need to slightly adjust your loop.
The simplest change would be to swap:
<ul>
{users.map(user => {
<li key={user.id}>
{user.title}
</li>
})}
</ul>
for
<ul>
{users.map(user => ( // <--- "{" becomes "("
<li key={user.id}>
{user.title}
</li>
))} // <--- "}" becomes ")"
</ul>
This will return an element to be rendered
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);
})
I am building a simple todo list. I have a form for adding a new todo list item and under it are listed all items in the todo list. When I add a new item through a form, I want to refresh the list of existing todo list items.
Items.jsx:
class Items extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [],
loading: true
};
}
componentDidMount() {
axios.get('/api/v1/items')
.then(response => {
this.setState({ items: response.data, loading: false });
});
console.log('state.items: '+this.state.items);
}
componentDidUpdate() {
axios.get('/api/v1/items')
.then(response => {
this.setState({ items: response.data, loading: false });
});
console.log('componentDidUpdate: '+this.state.items);
}
render() {
return (
<ItemSE.Group>
{
this.state.items.map(item => {
return <Item key={item.id} data={item} />
})
}
</ItemSE.Group>
);
}
}
export default Items
App.jsx:
class App extends Component {
constructor () {
super();
this.state = {
item_msg: ''
}
this.handleInputChange = this.handleInputChange.bind(this);
}
handleSubmit(e){
e.preventDefault();
console.log(this.state.item_msg);
axios.post('/api/v1/items', {
item: this.state.item_msg
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
}
handleInputChange(e) {
this.setState({ item_msg: e.target.value });
console.log('item_msg: '+this.state.item_msg);
}
render() {
return (
<div className="App">
<MainHeaderr />
<Container>
<NewItemForm
send_form={this.handleSubmit.bind(this)}
onInputChange={this.handleInputChange}
typed={this.state.item_msg} />
<Items />
</Container>
</div>
);
}
}
export default App;
I added componentDidUpdate to the Items.jsx file - when I add a new todo list, this new todo will indeed display immediately to the list - that's cool. However, I don't really feel this is the best practice.
When I look to the JS console, I see there hundreds of componentDidUpdate:.
Thus, what's the best way to refresh a list to todos?
This is one of the most challenging part for the newcomers into ReactJS.
You should not make stateful components at the every level.
Choose a common owner for the state. In your case Items component can't change it's state by itself without data from the parent App component, so there are no reasons to keep the state in this place.
Basically, you should keep items array and isLoading flag in the App component and then simply pass it into the Items as a prop.
Then, you may update your list by re-fetching data after adding new item on the backend or just add it into the list.
Also, you should update parent's App state on every input changes.
There are two ways:
You can keep it in NewItemForm state and then pass onSubmit into the parent event handler as a function prop.
Just make it uncontrollable and don't keep it in state at all and parent will take this param from event.target.value. (As it is now).
In both cases it won't re-render your list every time.
Because of this you should omit the handleInputChange from App component.
For example:
App.js
constructor(props) {
super(props);
// Initial state
this.state = {
items: [],
isLoading: false,
}
}
handleSubmit(e){
e.preventDefault();
const { value } = e.target;
this.setState({ isLoading: true });
axios.post('/api/v1/items', {
item: value
})
.then(response => {
// there are several ways - choose ONE of them
// 1. If server returns you the created item
// you can just add this item into the list
this.setState(prevState => {
return {
items: [...prevState.items, response.data],
isLoading: false,
}
});
// 2. But if there are any users who can make changing simultaneously with you
// (if not - just imagine it :) ) - it's better to make re-fetch data from server
axios.get('/api/v1/items')
.then(response => {
this.setState(prevState => ({ items: response.data, isLoading: false });
})
.catch(err => { console.log('Something bad is happened:', err) });
}
Finally, just pass data it into your Items component.
render() {
const { items, isLoading } = this.state;
return (
...
<Items items={items} isLoading={isLoading} />
...
)
}
I advice you to read this article if you haven't read it yet - https://reactjs.org/docs/thinking-in-react.html.
Hope it helps.
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.
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: []
}