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

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.

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);
})

reactjs -- Solving setState async problem

I've read this post: React setState not Updating Immediately
and realized that setState is async and may require a second arg as a function to deal with the new state.
Now I have a checkbox
class CheckBox extends Component {
constructor() {
super();
this.state = {
isChecked: false,
checkedList: []
};
this.handleChecked = this.handleChecked.bind(this);
}
handleChecked () {
this.setState({isChecked: !this.state.isChecked}, this.props.handler(this.props.txt));
}
render () {
return (
<div>
<input type="checkbox" onChange={this.handleChecked} />
{` ${this.props.txt}`}
</div>
)
}
}
And is being used by another app
class AppList extends Component {
constructor() {
super();
this.state = {
checked: [],
apps: []
};
this.handleChecked = this.handleChecked.bind(this);
this.handleDeleteKey = this.handleDeleteKey.bind(this);
}
handleChecked(client_id) {
if (!this.state.checked.includes(client_id)) {
let new_apps = this.state.apps;
if (new_apps.includes(client_id)) {
new_apps = new_apps.filter(m => {
return (m !== client_id);
});
} else {
new_apps.push(client_id);
}
console.log('new apps', new_apps);
this.setState({apps: new_apps});
// this.setState({checked: [...checked_key, client_id]});
console.log(this.state);
}
}
render () {
const apps = this.props.apps.map((app) =>
<CheckBox key={app.client_id} txt={app.client_id} handler={this.handleChecked}/>
);
return (
<div>
<h4>Client Key List:</h4>
{this.props.apps.length > 0 ? <ul>{apps}</ul> : <p>No Key</p>}
</div>
);
}
}
So every time the checkbox status changes, I update the this.state.apps in AppList
when I console.log new_apps, everything works accordingly, but console.log(this.state) shows that the state is not updated immediately, which is expected. What I need to know is how I can ensure the state is updated when I need to do further actions (like register all these selected strings or something)
setState enables you to make a callback function after you set the state so you can get the real state
this.setState({stateYouWant}, () => console.log(this.state.stateYouWant))
in your case:
this.setState({apps: new_apps}, () => console.log(this.state))
The others have the right answer regarding the setState callback, but I would also suggest making CheckBox stateless and pass isChecked from MyApp as a prop. This way you're only keeping one record of whether the item is checked, and don't need to synchronise between the two.
Actually there shouldn't be two states keeping the same thing. Instead, the checkbox should be stateless, the state should only be kept at the AppList and then passed down:
const CheckBox = ({ text, checked, onChange }) =>
(<span><input type="checkbox" checked={checked} onChange={() => onChange(text)} />{text}</span>);
class AppList extends React.Component {
constructor() {
super();
this.state = {
apps: [
{name: "One", checked: false },
{ name: "Two", checked: false }
],
};
}
onChange(app) {
this.setState(
previous => ({
apps: previous.apps.map(({ name, checked }) => ({ name, checked: checked !== (name === app) })),
}),
() => console.log(this.state)
);
}
render() {
return <div>
{this.state.apps.map(({ name, checked }) => (<CheckBox text={name} checked={checked} onChange={this.onChange.bind(this)} />))}
</div>;
}
}
ReactDOM.render(<AppList />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

React state is array of objects which are react elements

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: []
}

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