React State one state behind - reactjs

So I am having a very minimal program where on click of TouchableOpacity I add an item to my array. So the code for same looks like
// Initial state is []
onClick = () => {
this.setState(
{
arr: this.state.arr.concat({a: 1, b: 2}),
},
console.log(this.state.arr), // gives []
);
};
The console.log in the callback function for setState is behind one state. And on nextClick it gives the state as [{"a": 1, "b": 2}]. So how can I get the current updated state?

You are not passing console.log(...) inside a callback. Try the following :-
onClick = () => {
this.setState(
{
arr: this.state.arr.concat({a: 1, b: 2}),
},
()=>console.log(this.state.arr), //
);

I think you intend to pass a callback as a second argument but now you call console.log directly.
Just change it to () => console.log(this.state.arr),

Related

Error: Maximum update depth exceeded (react state)

I am trying to use update state in a react function component but it is not working. I tried following a tutorial on pluralsite and apply it to my own project. Ideally this code should be finding the product based on the ID number and replacing the total with a new value.
Unfortunately I am getting an error when setting the state. If I switch useState(productNew) with useState(data[index]) it seems like the error doesn't appear. They seem identical in structure and I'm unsure why I'm getting this issue.
This is the error message I get in the firefox console window (the message doesn't show up in Chrome). On top of the error message, the screen appears blank:
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops
Again, this happens when updating it with a new state, and the error does not occur when trying to set it to the original state value.
Are there any suggestions on how to solve this issue?
This is what the data looks like. In this example I am saving the first element of the array.
export let data = [
{
name: "Name",
description:
"",
products: [
{
id: 1,
name: "Name 1",
material: 1.05,
time: 25,
total: 0,
},
{
id: 2,
name: "Name 2",
material: 3,
time: 252,
total: 0,
},
],
},
...
];
function CompareCard({}) {
const index = 0;
const [productData, setProductData] = useState(data[index]);
function setTotalUpdate(id) {
const productPrevious = productData.products.find(function (rec) {
return rec.id === id;
});
const productUpdated = {
...productPrevious,
total: 1,
};
const productNew = productData.products.map(function (rec) {
return rec.id === id ? productUpdated : rec;
});
setProductData(productNew);
}
setTotalUpdate(1)
return null;
}
It looks like you are calling setState inside the body of your function component. This would cause your component to setState every time it is rendered, which then leads to another render, and another render... an infinite loop.
Instead, you should only call setState on events or within a useEffect hook.
Also, your Component needs to return some JSX, or null. It cannot return undefined.
function CompareCard({}) {
const index = 0;
const [productData, setProductData] = useState(data[index]);
function setTotalUpdate(id) {
const productPrevious = productData.products.find(function (rec) {
return rec.id === id;
});
const productUpdated = {
...productPrevious,
total: 1,
};
const productNew = productData.products.map(function (rec) {
return rec.id === id ? productUpdated : rec;
});
setProductData(productNew);
}
useEffect(() => {
// Only call setState within event handlers!
// Do not call setState inside the body of your function component
setTotalUpdate(1);
},[])
// You need to return JSX or null
return <p>productData: {JSON.stringify(productData)}</p>
}

Update useState immediately

useState does not update the state immediately.
I'm using react-select and I need to load the component with the (multi) options selected according to the result of the request.
For this reason, I created the state defaultOptions, to store the value of the queues constant.
It turns out that when loading the component, the values ​​are displayed only the second time.
I made a console.log in the queues and the return is different from empty.
I did the same with the defaultOptions state and the return is empty.
I created a codesandbox for better viewing.
const options = [
{
label: "Queue 1",
value: 1
},
{
label: "Queue 2",
value: 2
},
{
label: "Queue 3",
value: 3
},
{
label: "Queue 4",
value: 4
},
{
label: "Queue 5",
value: 5
}
];
const CustomSelect = (props) => <Select className="custom-select" {...props} />;
const baseUrl =
"https://my-json-server.typicode.com/wagnerfillio/api-json/posts";
const App = () => {
const userId = 1;
const initialValues = {
name: ""
};
const [user, setUser] = useState(initialValues);
const [defaultOptions, setDefaultOptions] = useState([]);
const [selectedQueue, setSelectedQueue] = useState([]);
useEffect(() => {
(async () => {
if (!userId) return;
try {
const { data } = await axios.get(`${baseUrl}/${userId}`);
setUser((prevState) => {
return { ...prevState, ...data };
});
const queues = data.queues.map((q) => ({
value: q.id,
label: q.name
}));
// Here there is a different result than emptiness
console.log(queues);
setDefaultOptions(queues);
} catch (err) {
console.log(err);
}
})();
return () => {
setUser(initialValues);
};
}, []);
// Here is an empty result
console.log(defaultOptions);
const handleChange = async (e) => {
const value = e.map((x) => x.value);
console.log(value);
setSelectedQueue(value);
};
return (
<div className="App">
Multiselect:
<CustomSelect
options={options}
defaultValue={defaultOptions}
onChange={handleChange}
isMulti
/>
</div>
);
};
export default App;
React don't update states immediately when you call setState, sometimes it can take a while. If you want to do something after setting new state you can use useEffect to determinate if state changed like this:
const [ queues, setQueues ] = useState([])
useEffect(()=>{
/* it will be called when queues did update */
},[queues] )
const someHandler = ( newValue ) => setState(newValue)
Adding to other answers:
in Class components you can add callback after you add new state such as:
this.setState(newStateObject, yourcallback)
but in function components, you can call 'callback' (not really callback, but sort of) after some value change such as
// it means this callback will be called when there is change on queue.
React.useEffect(yourCallback,[queue])
.
.
.
// you set it somewhere
setUserQueues(newQueues);
and youre good to go.
no other choice (unless you want to Promise) but React.useEffect
Closures And Async Nature of setState
What you are experiencing is a combination of closures (how values are captured within a function during a render), and the async nature of setState.
Please see this Codesandbox for working example
Consider this TestComponent
const TestComponent = (props) => {
const [count, setCount] = useState(0);
const countUp = () => {
console.log(`count before: ${count}`);
setCount((prevState) => prevState + 1);
console.log(`count after: ${count}`);
};
return (
<>
<button onClick={countUp}>Click Me</button>
<div>{count}</div>
</>
);
};
The test component is a simplified version of what you are using to illustrate closures and the async nature of setState, but the ideas can be extrapolated to your use case.
When a component is rendered, each function is created as a closure. Consider the function countUp on the first render. Since count is initialized to 0 in useState(0), replace all count instances with 0 to see what it would look like in the closure for the initial render.
const countUp = () => {
console.log(`count before: ${0}`);
setCount((0) => 0 + 1);
console.log(`count after: ${0}`);
};
Logging count before and after setting count, you can see that both logs will indicate 0 before setting count, and after "setting" count.
setCount is asynchronous which basically means: Calling setCount will let React know it needs to schedule a render, which it will then modify the state of count and update closures with the values of count on the next render.
Therefore, initial render will look as follows
const countUp = () => {
console.log(`count before: 0`);
setCount((0) => 0 + 1);
console.log(`count after: 0`);
};
when countUp is called, the function will log the value of count when that functions closure was created, and will let react know it needs to rerender, so the console will look like this
count before: 0
count after: 0
React will rerender and therefore update the value of count and recreate the closure for countUp to look as follows (substituted the value for count).This will then update any visual components with the latest value of count too to be displayed as 1
const countUp = () => {
console.log(`count before: 1`);
setCount((1) => 1 + 1);
console.log(`count after: 1`);
};
and will continue doing so on each click of the button to countUp.
Here is a snip from codeSandbox. Notice how the console has logged 0 from the intial render closure console log, yet the displayed value of count is shown as 1 after clicking once due to the asynchronous rendering of the UI.
If you wish to see the latest rendered version of the value, its best to use a useEffect to log the value, which will occur during the rendering phase of React once setState is called
useEffect(() => {
console.log(count); //this will always show the latest state in the console, since it reacts to a change in count after the asynchronous call of setState.
},[count])
You need to use a parameter inside the useEffect hook and re-render only if some changes are made. Below is an example with the count variable and the hook re-render only if the count values ​​have changed.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
The problem is that await api.get() will return a promise so the constant data is not going to have it's data set when the line setUserQueues(queues); is run.
You should do:
api.get(`/users/${userId}`).then(data=>{
setUser((prevState) => {
return { ...prevState, ...data };
});
const queues = data.queues.map((q) => ({
value: q.id,
label: q.name,
}));
setUserQueues(queues);
console.log(queues);
console.log(userQueues);});

How to add more elements in ReactJS state array?

this.state.counter.map is not a function I need to add object and map to counter
and I need to create and push to counter array and map to counter and show in this browser how to do that?
import React, { Component } from 'react';
import Counter from './counter';
class Counters extends Component {
state = {
counter: [
{value:0, id:1},
{value:0, id:2},
{value:0, id:3},
{value:0, id:4},
]
};
// function for create new array and map to him for delete one of counter in browser
DeleteButten = counterId => {
const counter = this.state.counter.filter(c =>c.id !== counterId);
this.setState({counter});
};
// this function for push new object to array and show in browser
AddCounter = () => {
const counter = this.state.counter.push({value:0 ,id:5});
console.log(this.state.counter);
this.setState({counter}); // error this.state.counter.map is not a function i need help to add object and map to counter
};
render() {
return (
<div>
{this.state.counter.map(counter => (
<Counter
key={counter.id}
onDelete{this.DeleteButten}
value={counter.value}
id={counter.id} selected={true}
/>
)}
<button onClick={this.AddCounter} className='btn btn-outline-info btn-sm m-2'> ADD result</button>
</div>
);
}
}
export default Counters;
In your code this line:
const counter = this.state.counter.push({value:0 ,id:5});
You're mutating the state array directly, which is not allowed in React.
You can either (shallow) clone the array and modify it:
// This does a shallow clone of the array.
const newCounter = [...this.state.counter];
// Modify the new array.
newCounter.push({ value: 0, id: 5 });
// Set the new array back to the state using setState.
this.setState({ counter: newCounter });
Or, you can use the shorter syntax for just appending new elements and making a new array at the same time:
// Append a new element to the new cloned array.
this.setState({ counter: [...this.state.counter, { value: 0, id: 5 });
// Or you can put the element at the front if you like :)
this.setState({ counter: [{ value: 0, id: 5 }, ...this.state.counter]);
The triple dot ([...array]) syntax is called spread, and can be used with both arrays and objects to conveniently clone or reconstruct new array or objects! Check out the MDN docs about it here.
One more little thing to improve is the above code works mostly, but note that React setState is async, there's a chance for race condition
Say for example you call your AddCounter method multiple times within the same event loop, the later results may override the previous ones.
Therefore, if you are setting something that depends on the current state, it's recommended to use the callback syntax:
this.setState(state => { counter: [...state.counter, { value: 0, id: 5 }] });
See more details and examples of the async nature of setState in React docs.
If you just want to add element to array here is the code :
this.setState({counter:...this.state.counter,new_count})
new_count is what you are trying to push

cannot access the data inside an array

I'm having trouble accessing data inside my state which I fetch from my database,
this is my state:
state = {
inputDataArr: [],
inputDataArrTest: [
{
formID: 'test',
inputArr: [1, 2, 3],
labelArr: [4, 5, 6]
}
]
};
this is the collection I'm importing from the database:
{
"_id": {
"$oid": "5ba96b8ebffd923734090df4"
},
"inputArr": [
"value1",
"value2",
"value3"
],
"labelArr": [
"label1",
"label2",
"label3"
],
"formID": "5ba96b83bffd923734090df0",
"__v": 0
}
which is an object with 2 arrays in it,
this is how I fetch it :
componentWillMount() {
fetch('/api/datas')
.then(data => data.json())
.then(datas => {
const filterdDatas = datas.filter(
data => data.formID == this.props.match.params.id
);
this.setState(currentState => ({
inputDataArr: [...currentState.inputDataArr, ...filterdDatas]
}));
});
}
now when I console log inputDataArr and inputDataArrTest I get an array with an object inside,
exactly the same,
but when I console log InputDataArrTest[0] which is what you can see in my state I can see the arrays and the formID,
but when I console log inputDataArr[0] I get undefined, really frustrating, anybody knows why?
Mapping and doing data manipulation inside the render is never a good idea and isn't easy to debug.
My suggestion is to initialize the state to an empty array, and call a method to return the mapped data.
constructor(props) {
super(props);
this.state = {
inputDataArr: []
};
}
render() {
const data = this.state.inputDataArr.map(...)
return <div>{data}</div>
}
This way state is initiated before first render and data will return an empty array.
After componentDidMount the state will update and will re-render with the new data.
As a side note, i would do the mapping in a third then and set the state to the result itself.
Happy Coding.
I'm not sure where are you running the console.log but setState has a callback method that you can run after the actual data has been updated:
this.setState(currentState => ({
inputDataArr: [...currentState.inputDataArr, ...filterdDatas]
}), () => {
// this is the callback for setState, you can access the updated data
console.log(inputDataArr);
});
Note If you run the console.log in render keep in mind that the first render call will always run before the async data has been fetched.
Note #2 Do not use componentWillMount at all and especially not for fetching data.
Use componentDidMount instead.
Have you test filteredDatas before using ...filteredDatas ? Maybe it is not an Array and result to other thing or an empty Array.
Are you sure this.props.match.params.id is defined and equal to the formID property?
If it exists only one object with unique formID why don’t you use Array#find instead of Array.filter and then update your state with
this.setState(prevState => ({
dataArr: filteredDatas ? prevState.dataArr.concat([filteredDatas]) : prevState.dataArr // Here with Array#find, filteredDatas is null or a object not an Array
});
renderLabels() {
const { inputDataArr } = this.state;
return (
!!inputDataArr.length &&
inputDataArr[0].labelArr.map((label, index) => (
<th key={index}>{label}</th>
))
);
}
conditional rendering solved my problem

callback in setState doesn't seem to work

I have the following:
addTodo() {
const text = prompt("TODO text please!")
this.setState({todos:[...this.state.todos,
{id:id++,
text: text,
checked:false}]})
console.log(this.state)
}
the console shows an empty array which makes sense as setState is asyncronous. I change the function to use a callback:
addTodo() {
const text = prompt("TODO text please!")
this.setState(function(prevState){
return {todos: [...prevState.todos,
{id: id++,
text: text,
checked: false} ]}
})
console.log(this.state)
}
console.log is still showing an empty array. Doesn't the use of the callback update setState?
setState function's second argument should be the function which need to be called after setting the state. So you should pass callback as second argument like this
this.setState({
todos:[
...this.state.todos,
{id:id++,text: text,checked:false}
]
},() => {console.log(this.state)})

Resources