React Incremeting Counter Behind - reactjs

I've just learned to use UseState to create a simple incremental counter, but I've noticed some odd behavior with the count being 2 numbers behind (+ - ) in console.log. Now on the screen the number displays fine, but this creates an issue because I'm trying to change the color of the number if it's negative or positive.
Because I'm trying to change the display color of the number on the screen, would UseEffect be a good solution to this problem? I'm going to go back and watch some YT videos on UseEffect, but figured I'd ask here as well. I was thrilled when I was able to figure out how to change the classnames using state, but then got a pie in the face when the numbers weren't changing colors correctly.
Here's an example of the behavior I'm seeing.
const { useState } = React
function Vote () {
const [count, setCount] = useState(0)
const [color, setColor] = useState('black')
function handleDecrement () {
setCount(count - 1)
checkCount()
}
function handleIncrement () {
setCount(count + 1)
checkCount();
}
function checkCount () {
// Less than 0 make it red
if (count < 0) {
setColor('red')
console.log(count)
// Greater than 1 make it green
} else if (count > 0 ) {
setColor('green')
console.log(count)
// If it's 0 just keep it black
} else {
setColor('black')
console.log(count)
}
};
return (
<div>
<button onClick={handleDecrement}>-</button>
<h1 className={color}>{count}</h1>
<button onClick={handleIncrement}>+</button>
</div>
)
}
ReactDOM.render(<Vote />, document.getElementById('root'))

Yes, you can simply use an effect hook with dependency to check the color. When count updates the effect hook callback is triggered.
The issue is that react state updates are asynchronous, so the updated state count won't be available until the next render cycle; you are simply using the count value from the current render cycle.
Note: When incrementing/decrementing counts you should use a functional state update. This ensures state is correctly updated from the previous state in the case multiple state updates are enqueued within any single render cycle.
function Vote() {
const [count, setCount] = useState(0);
const [color, setColor] = useState("black");
function handleDecrement() {
setCount(count => count - 1);
}
function handleIncrement() {
setCount(count => count + 1);
}
useEffect(checkCount, [count]);
function checkCount() {
// Less than 0 make it red
if (count < 0) {
setColor("red");
console.log(count);
// Greater than 1 make it green
} else if (count > 0) {
setColor("green");
console.log(count);
// If it's 0 just keep it black
} else {
setColor("black");
console.log(count);
}
}
return (
<div>
<button onClick={handleDecrement}>-</button>
<h1 className={color}>{count}</h1>
<button onClick={handleIncrement}>+</button>
</div>
);
}

When updating the state based on the current state always use the callback version of setState which receives the current state as an argument and should return the next state. React batches state updates and relying on what has been returned by useState to update can yield incorrect results. Also the way to check for a change to count and update accordingly is by using useEffect with count as a dependency. The console.log() in your example will still log the old state as state updates are async and can only be seen during the next render.
const [count, setCount] = useState(0)
const [color, setColor] = useState('black')
function handleDecrement () {
setCount(current => current - 1);
}
function handleIncrement () {
setCount(current => current + 1)
}
useEffect(() => {
// Less than 0 make it red
if (count < 0) {
setColor('red')
console.log(count)
// Greater than 1 make it green
} else if (count > 0 ) {
setColor('green')
console.log(count)
// If it's 0 just keep it black
} else {
setColor('black')
console.log(count)
}
}, [count]);

Related

How do I loop an image carousel with React setState and setInterval?

I am trying to set up an image carousel that loops through 3 images when you mouseover a div. I'm having trouble trying to figure out how to reset the loop after it reaches the third image. I need to reset the setInterval so it starts again and continuously loops through the images when you are hovering over the div. Then when you mouseout of the div, the loop needs to stop and reset to the initial state of 0. Here is the Code Sandbox:
https://codesandbox.io/s/pedantic-lake-wn3s7
import React, { useState, useEffect } from "react";
import { images } from "./Data";
import "./styles.css";
export default function App() {
let timer;
const [count, setCount] = useState(0);
const updateCount = () => {
timer = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
if (count === 3) clearInterval(timer);
};
const origCount = () => {
clearInterval(timer);
setCount((count) => 0);
};
return (
<div className="App">
<div className="title">Image Rotate</div>
<div onMouseOver={updateCount} onMouseOut={origCount}>
<img src={images[count].source} alt={images.name} />
<p>count is: {count}</p>
</div>
</div>
);
}
Anything involving timers/intervals is an excellent candidate for useEffect, because we can easily register a clear action in the same place that we set the timer using effects with cleanup. This avoids the common pitfalls of forgetting to clear an interval, e.g. when the component unmounts, or losing track of interval handles. Try something like the following:
import React, { useState, useEffect } from "react";
import { images } from "./Data";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
const [mousedOver, setMousedOver] = useState(false);
useEffect(() => {
// set an interval timer if we are currently moused over
if (mousedOver) {
const timer = setInterval(() => {
// cycle prevCount using mod instead of checking for hard-coded length
setCount((prevCount) => (prevCount + 1) % images.length);
}, 1000);
// automatically clear timer the next time this effect is fired or
// the component is unmounted
return () => clearInterval(timer);
} else {
// otherwise (not moused over), reset the counter
setCount(0);
}
// the dependency on mousedOver means that this effect is fired
// every time mousedOver changes
}, [mousedOver]);
return (
<div className="App">
<div className="title">Image Rotate</div>
<div
// just set mousedOver here instead of calling update/origCount
onMouseOver={() => setMousedOver(true)}
onMouseOut={() => setMousedOver(false)}
>
<img src={images[count].source} alt={images.name} />
<p>count is: {count}</p>
</div>
</div>
);
}
As to why your code didn't work, a few things:
You meant to say if (count === 2) ..., not count === 3. Even better would be to use the length of the images array instead of hardcoding it
Moreover, the value of count was stale inside of the closure, i.e. after you updated it using setCount, the old value of count was still captured inside of updateCount. This is actually the reason to use functional state updates, which you did when you said e.g. setCount((prevCount) => prevCount + 1)
You would have needed to loop the count inside the interval, not clear the interval on mouse over. If you think through the logic of it carefully, this should hopefully be obvious
In general in react, using a function local variable like timer is not going to do what you expect. Always use state and effects, and in rarer cases (not this one), some of the other hooks like refs
I believe that setInterval does not work well with function components. Since callback accesses variables through closure, it's really easy to shoot own foot and either get timer callback referring to stale values or even have multiple intervals running concurrently. Not telling you cannot overcome that, but using setTimeout is much much much easier to use
useEffect(() => {
if(state === 3) return;
const timerId = setTimeout(() => setState(old => old + 1), 5000);
return () => clearTimeout(timerId);
}, [state]);
Maybe in this particular case cleanup(clearTimeout) is not required, but for example if user is able to switch images manually, we'd like to delay next auto-change.
The timer reference is reset each render cycle, store it in a React ref so it persists.
The initial count state is closed over in interval callback scope.
There are only 3 images so the last slide will be index 2, not 3. You should compare against the length of the array instead of hard coding it.
You can just compute the image index by taking the modulus of count state by the array length.
Code:
export default function App() {
const timerRef = useRef();
const [count, setCount] = useState(0);
// clear any running intervals when unmounting
useEffect(() => () => clearInterval(timerRef.current), []);
const updateCount = () => {
timerRef.current = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
};
const origCount = () => {
clearInterval(timerRef.current);
setCount(0);
};
return (
<div className="App">
<div className="title">Image Rotate</div>
<div onMouseOver={updateCount} onMouseOut={origCount}>
<img
src={images[count % images.length].source} // <-- computed index to cycle
alt={images.name}
/>
<p>count is: {count}</p>
</div>
</div>
);
}
Your setCount should use a condition to check to see if it should go back to the start:
setCount((prevCount) => prevCount === images.length - 1 ? 0 : prevCount + 1);
This will do setCount(0) if we're on the last image—otherwise, it will do setCount(prevCount + 1).
A faster (and potentially more readable) way of doing this would be:
setCount((prevCount) => (prevCount + 1) % images.length);

Why the count value increment by 1 not by 2 with two setState in React

Here's the code that does not work :
import { useState } from "react";
const Counter = () => {
const [count,setCount]= useState(0);
const buttonHandler = ()=>{
setCount(count+1);
setCount(count+1);
}
return (
<div >
{count}
<button onClick={buttonHandler}>+</button>
</div>
);
}
I don't really get what is happening under the hood of React. I saw through some videos it would work if I did this:
const buttonHandler = ()=>{
setCount(prevCount => prevCount+1);
setCount(prevCount => prevCount+1);
}
But I don't feel like I really get why the first one is not working
In your function, buttonHandler, setCount isn't changing the value of count immediately. It's just updating the state that is used to set count the next time Counter is rendered.
So if count is 0, calling setCount with a value of count + 1 twice actually results in two calls to setCount with a value of 1.

React hooks: Issue with React.memo and useState

I played around with react hooks, and I came across an issue that I do not understand.
The code is here https://codesandbox.io/embed/hnrch-hooks-issue-dfk0t
The example has 2 simple components:
App Component
const App = () => {
const [num, setNum] = useState(0);
const [str, setStr] = useState();
const inc = () => {
setNum(num + 1);
setStr(num >= 5 ? ">= 5" : "< 5");
console.log(num);
};
const button = <button onClick={inc}>++</button>;
console.log("parent rerender", num);
return (
<div className="App">
<h1>App</h1>
<Child str={str}>{button}</Child>
</div>
);
};
Child Component
const Child = React.memo(
({ str, children }) => {
console.log("child rerender");
return (
<div>
<>
<h2>Functional Child</h2>
<p>{str}</p>
{children}
</>
</div>
);
}
(prev, props) => {
return prev.str === props.str;
}
);
So I have the child wrapped in a React.memo, and it should only rerender if str is different. But is also has children, and it get's passed a button which is incrementing a counter inside the parent (App).
The issue is: The counter will stop incrementing after it is set to 1. Can someone explain this issue to me and what I need to understand to avoid those bugs?
It's a "closure issue".
This is the first render time for App, the inc function has been created the first time: (let's call it inc#1)
const inc = () => {
setNum(num + 1);
setStr(num >= 5 ? ">= 5" : "< 5");
console.log(num);
};
In the inc#1 scope, num is currently 0. The function is then passed to button which is then passed to Child.
All good so far. Now you press the button, inc#1 is invoked, which mean that
setNum(num + 1);
where num === 0 happen. App is re-rendered, but Child is not. The condition is if prev.str === props.str then we don't render again Child.
We are in the second render of App now, but Child still own the inc#1 instance, where num is 0.
You see where the problem is now? You will still invoke that function, but inc is now stale.
You have multiple ways to solve the issue. You could make sure that Child has always the updated props.
Or you could pass a callback to setState to fetch the current value ( instead of the stale one that live in the scope of the closure ). This is also an option:
const inc = () => {
setNum((currentNum) => currentNum + 1);
};
React.useEffect(() => {
setStr(num >= 5 ? ">= 5" : "< 5");
}, [num])
A couple of things here.
If you are modifying a state and its new value depends on the previous value of the state, use setState's functional form:
setNum(num => num + 1);
setState is async, so when you try to setStr, the num value is not updated yet. Even more, in your particular case inc is closing over (i.e. creates a closure) num value from the state, so inside that function it'll always have its initial value - 0.
To fix this you need to use the Effect hook to update the string when num changes:
useEffect(() => {
setStr(num >= 5 ? ">= 5" : "< 5");
}, [num]) // Track the 'num' var here
One way to fix the bug was changing inc function to this:
const inc = () => {
setNum(n => {
const newNum = n + 1;
setStr(newNum >= 5 ? ">= 5" : "< 5");
return newNum;
});
};
Note that setState is now passed a callback function, which receives the old value and returns the new one. That way, the "closure" issue was resolved.

React Function Component counter with hooks

I'm trying to understand the new React hooks and their use cases.
My goal is a single component that counts up and also every x tick counts another counter.
I have achieved it using useEffect and useState, with two main problems:
1. A memory leak when the component unmounts before the timeout gets called (when navigating using react-router)
2. The component renders twice on every tick because useEffect and useState both trigger the render.
I think the solution will be something with useRef or useMemo but I haven't figured it out yet.
My current component (with typescript):
import React from "react";
const Component: React.FC = () => {
const [trigger, setTrigger] = React.useState(0);
const [timer, setTimer] = React.useState({ cycle: 0, count: 0 });
let refTimer = React.useRef({ cycle: 0, count: 0 });
// useRef
// React.useEffect(() => {
// setInterval(() => {
// console.log("tick");
// if (refTimer.current.count % 2 === 0) {
// refTimer.current.cycle++;
// setTimer(refTimer.current);
// }
// refTimer.current.count++;
// setTimer(refTimer.current);
// // console.log(timer);
// }, 1000);
// }, []);
// useState
React.useEffect(() => {
console.log("effect tick");
setTimeout(() => {
console.log("tick");
const count = timer.count + 1;
if (count % 2 === 0) {
const cycle = timer.cycle + 1;
setTimer({ ...timer, count, cycle });
return;
}
setTimer({ ...timer, count });
}, 1000);
}, [timer]);
return (
<div>
<br />
<br />
<br />
<br /> Playground:
<div>Count: {timer.count}</div>
<div>Cycle: {timer.cycle}</div>
<button type="button" onClick={(): void => setTrigger(trigger + 1)}>
Trigger Count: {trigger}
</button>
</div>
);
};
export default Component;
As I said, like this I have the mentioned two problems. I can remove the useEffect entirely that would fix the double render but when I click the Trigger Button the ticks will stack up which is worse than double renders.
The commented useRef part is what I have tried but it somehow doesn't work.
I appreciate all the help!
Edit:
A third minor problem is that like this the counter runs only with setTimeout which will trigger another setTimeout, so if that process takes some time it won't really be an exact interval.
So my goal is an interval that runs in a separate process (I'd say inside a useEffect) what will cause a rerender on every tick and won't stack up on each call or when something else triggers a rerender.
You can fix the memory leak mentioned in #1.
React.useEffect(() => {
console.log("effect tick", timer);
// .. 👇 get the timeout ID to clear on unmount
const id = setTimeout(() => {
console.log(`tick id=${id}`, timer);
const count = timer.count + 1;
if (count % 2 === 0) {
const cycle = timer.cycle + 1;
setTimer({ ...timer, count, cycle });
return;
}
setTimer({ ...timer, count });
}, 1000);
// ... 👇 Clean up here with the ID on unmount
return () => clearTimeout(id);
}, [timer]);
Regarding #2 double render, would you be more specific?
Before/After cleaning up in useEffect above, I am unable to figure out what you mean as the current console logs seem to work as expected.

State getting reset to 0 after render

I am rendering photos from unsplash api. And I am keeping the index of the photos to be used in the lightbox, after the initial render state of imageindex goes back to 0, how can I retain its value?
I will show some code
const ImageList = ({ image, isLoaded }) => {
const [imageIndex, setImageIndex] = useState(0);
const [isOpen, setIsOpen] = useState('false');
const onClickHandler = (e) => {
setIsOpen(true);
setImageIndex(e.target.id);
};
const imgs = image.map((img, index) => (
<img
id={index}
key={img.id}
src={img.urls.small}
onClick={onClickHandler}
if (isOpen === true) {
return (
<Lightbox
onCloseRequest={() => setIsOpen(false)}
mainSrc={image[imageIndex].urls.regular}
onMoveNextRequest={() => setImageIndex((imageIndex + 1) % image.length)}
onMovePrevRequest={() => setImageIndex((imageIndex + image.length - 1) % image.length)}
nextSrc={image[(imageIndex + 1) % image.length].urls.regular}
prevSrc={image[(imageIndex + image.length - 1) % image.length].urls.regular}
/>
after the initial render state, imageIndex goes back to 0.
That makes sense, the initial render would use whatever you set as the default value. You can use something like local storage to help you keep track of the index of the last used item. It's a bit primitive, but until you integrate something like Node/MongoDB for database collections, this will be perfect.
In your component, import useEffect() from React. This hook lets us execute some logic any time the state-index value changes, or anything else you might have in mind.
import React, { useEffect } from "react"
Then inside your component, define two useEffect() blocks.
Getting last used index from localStorage on intitial load:
useEffect(() => {
const lastIndex = localStorage.getItem("index", imageIndex)
setImageIndex(imageIndex)
}, []) //set as an empty array so it will only execute once.
Saving index to localStorage on change:
useEffect(() => {
localStorage.setItem("index", imageIndex)
}, [imageIndex]) //define values to subscribe to here. Will execute anytime value changes.

Resources