I am practicing React using Hooks and Context, working on a simple Quiz App. The score should increment to 1 when the answer is correct.
const { qa, questionNumber } = useContext(GlobalContext);
const [score, setScore] = useState(0);
const answerOnClick = (e) => {
const correct = qa[questionNumber].correct_answer === e ? true : false;
if (correct) {
setScore(() => score++);
}
};
But I'm getting this error on line setScore(() => score++);:
TypeError: Assignment to constant variable
I also tried if (correct) { score++; setScore(() => score); } and setScore(() => ++score), still not working.
But when I try setScore(() => score + 1);, now it increments!
I have learned that the Increment is a valid JS operator. Aren't score++ and score + 1 equivalent? And why score treat as a constant variable? It is mutable, right? I'm still a novice developer. Can someone explain what's happening here? Thank you.
The problem is, that score is defined as constant. This means that is should not be reassigned. When using score++ you're reassigning it as follows score = score + 1 but as a short hand. Your JavaScript interpreter does not like that you're reassigning a variable which you defined as being constant. Therefore you get the error.
The useState hook provides an update function (in your case setScore) which you should use to update the state. You're not directly changing the value of score. You're telling react to initialise score with a higher value on next render. Your components function is called again with a new declaration of score, this time with a higher value. As you correctly pointed out setScore(() => score + 1) works, however, setScore(score + 1) should work too.
You mutate the state when you do score++. You should not mutate the state in React because setNum is async.
https://reactjs.org/docs/react-component.html#setstate
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
score++/++score would mutate the state. The best way to update state based on previous state is this way:
setScore((prevScore) => prevScore + 1);
Related
I am making a NewsCardComponent which will display slideshow of images provided in an array which will be passed down as a prop. Everytime the component is used, will have a different number of elements in images array. so i put the "imgArr" in src of img as:
<img src={imgArr[index]}>
where "index" is the state and i have to dynamically check if a component has reached to the end of array then setIndex to zero. I have achieved what I wanted but i dont know why all the techniques other than first are not working.
My useEffect:
useEffect(() => {
const interval = setInterval(() => {
indexResetter();
}, 2000);
return () => {
clearInterval(interval);
};
}, []);
Technique 1 (working fine) :
function indexResetter() {
setIndex((prev) => {
let newIndex = prev + 1;
if (newIndex > imgArr.length - 1) {
newIndex = 0;
}
return newIndex; }); }
Technique 2 (state is not setting to zero but increasing infinitely):
function indexResetter() {
let newIndex = index + 1;
if (newIndex === imgArr.length - 1) {
setIndex(0);
} else {
setIndex((prev) => prev + 1);
}
}
Technique 3 (same problem with second one):
function indexResetter() {
if (index >= imgArr.length - 1) {
setIndex(0);
} else {
setIndex((prev) => prev + 1);
}
}
In short, your useEffect() runs on initial mount, meaning that the setInterval() continues to execute the indexResetter function that was defined on the initial render, even after subsequent rerenders when new indexResetter have been created. That means the version of the indexResetter function that you end up executing only knows about the index state upon the initial mount, giving you the issue where your index doesn't change.
For more details, when you define a function, it stores a reference to the "scope" it's defined in, which includes all the variables defined in that scope:
function NewsCardComponent() {
const [index, setIndex] = useState(0);
function indexResetter() {
...
}
}
When your component above renders, it does the following:
The NewsCardComponent function gets called, creating a new "scope" (formally an environment record). This scope holds the variables and functions (bindings) such as index created within the function.
The indexResetter function gets created (note: it's just being created, it's not being called yet). This function stores an internal reference to the scope created in step 1. This is called a closure.
Later on, when indexResetter gets called, it uses the scope that it stored internally at step 2 to work out the value of the index variable.
When you update your index state using setIndex(), your component rerenders, and performs the above two steps 1 and 2 again. When this occurs, it creates a new scope for the NewsCardComponent that now holds the updated value of index, as well as creates a new indexResetter function. This means that each time you call setIndex, you effectively create new versions of the indexResster function that can see the new value of index. The value of index in the previous NewsCardComponent scope is still what it was before, and so the indexResetter function created in the previous render can still only see the old index value. The new index value is only available in the newly created scope that was created as part of the rerender.
Your problem is that your useEffect() only runs on the initial mount of your component, so the function that you're calling within your setInterval() is the first indexResetter function that was created on the initial mount. As a result, it only has visibility of the value of index for when your component initially mounted:
const interval = setInterval(() => {
indexResetter();
}, 2000);
On subsequent rerenders, the indexResetter function will be recreated, but the above setInterval() will continue to call the version of the indexResster function that was defined on the initial render (again due to a closure), which only knows about the index state at that time. As a result, the value of index within the function ends up always being the initial value.
In your working example, you're using the state setter function to access prev, which means you're no longer relying on the index value from the surrounding scope of your function. The state setter function will provide you with the most up-to-date value of your state, and so you don't face the same issue.
From what I am reading, your use effect only runs during the first render. Put index in the dependency array in your useEffect so that it runs everytime that your index changes
#robinmuhia254 is right!
The index should be passed as a dependency to the useEffect for the change to run whenever the index changes. In that case, all your techniques will result in the same output.
If you do not pass the index dependency, Techniques 2 and 3 will not meet the if-crieria(index doesn't have an updated value, it's 0 always), and the setTimeout will run the else part to increase the counter. Technique 1 works in this case because you do not bank on the index state variable at all for your computations. You compute based on the prev value.
I hope that explains!
Btw, Technique 2 has got a logical error. If you set newIndex = index + 1, the carousel will start-over before it reaches the last element.
I have made a small demo on your use case where I have also fixed the Technique 2 logic. Please have a look:
https://stackblitz.com/edit/react-1mbtpm?file=src/App.js
I have a component which renders only once and then is being destroyed,
I've implemented the "Comp3" way.
Using "useEffect" to calculate the str with the num value I received as a prop, then stored the result using "useEffect" setter in order to use the result in the html.
//Let say this function is inside the functional component
function numToStr(num:number):string{
switch(num){
case 1:
return "One";
case 2:
return "Two";
...
}
}
function Comp1({num:number}){
const numAsStr = numToStr(num);
return <span>{numAsStr}</span>
}
function Comp2({num:number}){
const [numAsStr] = useState(numToStr(num));
return <span>{numAsStr}</span>
}
function Comp3({num:number}){
const [numAsStr,setNumAsStr] = useState();
useEffect(()=>{
setNumAsStr(numToStr(num));
},[])
return <span>{numAsStr}</span>
}
function Comp4({num:number}){
const numAsStr = useMemo(()=>numToStr(num),[]);
return <span>{numAsStr}</span>
}
My question is:
What is the best solution in terms of best-practice/ "react-way"?
How each implementation effect the performance?
Does the fact that I know the component only renders once should impact way I choose to implement?
Or should I treat this component as if i don't know it should be render once and in this case still support the option to "watch" over the prop being changed? (add it to the useEffect / useMemo arrays)
Thanks!
If the calculations being done by strToNum are cheap (as they are in your simplified example), then the approach in Comp1 is probably the best and simplest. They'll run each time the component re-renders, so they're always up-to-date.
If the calculations are expensive, the recommended way to deal with this is the useMemo hook, like in your Comp4 example. However, you'd want to make sure to include num input variable in your dependents array so that numAsStr gets re-computed whenever num changes. E.g:
function Comp4({num:number}){
const numAsStr = useMemo(()=>numToStr(num),[num]);
return <span>{numAsStr}</span>
}
Using useState as you have in Comp2 would only run numToStr in the initial render, so you'd get stale values if num ever changed.
Using useEffect as you have in Comp3 introduces an unnecessary double-render - e.g. it renders first without a value for numAsStr and then renders again.
I know you said that you are currently sure that it never re-renders again - so some of the downsides/gotchas mentioned above might not apply in this case (and then I'd just go with the Comp1 approach, because it's simpler) but in my experience, it's best not to make that assumption - will you (or a team-mate) remember that in a month when you try to refactor something?
I have researched other cases when the state was unidentified but I am still unsuccessfully trying to subtract data from UI actions that is in the format [{},{}...]. I have managed to add to the array , using this code, which in the same time also computes the total for the item (subTotal and products are props from a child component got through a callback function):
const updateTotalPriceAndUpdatePieChartData = (subTotal,product) => {
//from here
setPieChartData([...pieChartData,{product,subTotal}])
//until up, we handle what data we need for the pieChart
setTotal(total => total + (Number.isFinite(subTotal) ? subTotal : 0))
console.log("TOTAL WAS COMPUTED")
}
And this is the state that holds the array:
const [pieChartData,setPieChartData]=React.useState([])
Yet when I try to delte an object from the state array (when an item is also deleted), I try the following :
const substractSubTotalAndSubstractTotalForPieChart = (subTotal,product,pieChartData) => {
setTotal(total - subTotal)
const lastPieChartData=pieChartData.filter(item => item !={subTotal,product})
setPieChartData(lastPieChartData)
}
It says that pieChartData is unidentified. Could you please let me know what I can do?
In substractSubTotalAndSubstractTotalForPieChart method, there is a paramter pieChartData, but there is also a state with the same name. So what happens is inside the function substractSubTotalAndSubstractTotalForPieChart's scope, the parameter pieChartData is given preference rather than the state.
Hence in your method invocation, when you don't pass this parameter, the default value of an uninitialized parameter, i.e., undefined is used.
Example Code:
(Pretend we are inside component function)
let count = useRef(0).current
useEffect(()=>{
count++
console.log(count)
}, [count])
Question:
What will happen if I run this code
(I am afraid that the endless loop execution will blow up my m1 chip macbookair, so I didn't run ^_^).
Should I awalys some_ref_object.curent = some_value to change the value?
The code probably will not do what you expect.
useRef returns a mutable object, where the object is shared across renders. But if you extract a property from that object into a variable and then reassign that variable, the object won't change.
For the same reason, reassigning the number below doesn't change the object:
const obj = { foo: 3 };
let { foo } = obj;
foo = 10;
console.log(obj);
In your code, the ref object never gets mutated. It is always the following object:
{ current: 0 }
So
let count = useRef(0).current
results in count being initially assigned to 0 at the beginning of every render.
This might be useful if for some odd reason you wanted to keep track of a number inside a given render, not to persist for any other render - but in such a case, it'd make a lot more sense to remove the ref entirely and just do
let count = 0;
Your effect hook won't do anything useful either - since count is always 0 at the start of every render, the effect callback will never run (except on the first render).
Should I awalys some_ref_object.curent = some_value to change the value
You should use current, not curent. But yes - if you want the change to persist, assign to a property of the object, instead of reassigning a standalone variable. (Reassigning a standalone variable will almost never have any side-effects.)
But, if you have something in the view that the count is used in - for example, if you want to return it in the JSX somewhere - you probably want state instead of a ref (or a let count = 0;), so that setting state results in the component re-rendering and the view updating.
I just tried it on my colleague's computer, and fortunately it didn't blow up
Conclusion 1:
The useEffect won't effect, because ref can't be denpendency.
Conclusion 2:
Only let count = useRef(0).current is the right way.
handleClick(event) {
let value = event.target.value;
this.setState({ question: (this.state.question += value) });
I get a warning:
Do not mutate state directly. Use setState()
react/no-direct-mutation-state
if I try to load the page with this code.
How can I fix it so it doesn't give me this warning?
It says to use this.setState, but that's what I'm doing.
You're doing an unnecessary assignment addition to this.state.question - you only need addition here. Furthermore, when updating state based on a previous value, the docs state:
React may batch multiple setState() calls into a single update for performance. Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
The proper way to update state based on a previous value is to pass in an update function that works off the previous value, i.e.:
this.setState(prevState => ({ question: prevState.question + value }));
The mistake is here :
this.state.question += value
+= is the operator used to add right operand to the left operand. You are trying to add value to this.state.question.
The correct way to do it is :
this.setState(previousState => ({ question: previousState.question + value }));
This is considered mutating state because you are assigning a value to this.state.question in the expression (this.state.question += value). I have pasted a link to a Code Sandbox you can view to see how to correctly implement this behavior.
Code Sandbox