I have a for loop in which I'm trying to add a boolean true
after 300ms. However, after the first one, the second and third boolean get added immediately together.
//imports
export default function Card(){
const [show, setShow] = useState<boolean[]>([]);
useEffect(() => {
async function showSections() {
if (show.length === renderRoutine.sections.length) return;
console.log("SHOW LENGTH: ", show.length, "ROUTINE LENGTH: ", renderRoutine.sections.length);
const delay = show.length > 0 ? 300 : 0;
await timer(delay); // wait if not first
setShow((show) => [...show, true]);
}
showSections();
}, [show]);
return (
{renderRoutine.sections.map(
(section, idx) => (
console.log("SHOW IDX", idx ":", show[idx]),
console.log("SHOW ARRAY: ", show),
(
<Transition
key={section.name}
as={Fragment}
appear={true}
show={show[idx] === undefined ? false : show[idx]}
enter="transition ease-in duration-150"
enterFrom="transform opacity-0 scale-100 -translate-x-3"
enterTo="transform opacity-100 scale-100"
leave="transition ease-out duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-100 translate-x-2"
>
//Card elements
</Transition>
)};
I also made a screen recording which would explain it better. In there you can see that the useState array gets filled with 5(!) booleans. How is this possible?
EDIT: updated useEffect with Ori Drori's answer. Underneath; a screenshot of the console.
EDIT 2: Solution was setting up an if statement for the first value.
const [show, setShow] = useState<boolean[]>([]);
useEffect(() => {
async function showSections() {
if (show.length > renderRoutine.sections.length) return;
console.log("SHOW LENGTH: ", show.length, "ROUTINE LENGTH: ", renderRoutine.sections.length);
const delay = show.length > 0 ? 100 : 0;
await timer(delay); // wait if not first
if (show.length === 0) {
setShow([true]);
} else setShow((show) => [...show, true]);
console.log(show);
}
showSections();
}, [show]);
CodePudding user response:
If you're using React 18, the state updates are probably batched together. Check by increasing the timeout a lot (2000 for example). If this works now, then batching is the cause.
A "simple" solution would be to use flushSync to force a render. However, as stated in the docs:
Using flushSync is uncommon and can hurt the performance of your app.
In addition, it won't work during a render cycle - for example calling it inside useEffect
, so you'll need to wrap it in a timeout.
flushSync(() => {
setShow((show) => [...show, true]);
});
A better solution would be to restructure your code. Whenever you update show
, and cause a re-render, check if the length of show
is less than renderRoutine.sections.length
. If it is, wait (if not first item), and then update show
, which would cause a re-render...
const [show, setShow] = useState < boolean[] > ([]);
useEffect(() => {
async function showSections() {
// renderRoutine.sections.length = 5
if(show.length < renderRoutine.sections.length) {
if(!!show.length) await timer(300); // wait if not first
setShow(show => [...show, true]);
}
showSections();
}, [show]);
Working example - I can't use async/await in a snippet, so I using useTimeout
instead:
const { useState, useEffect } = React;
const sections = [1, 2, 3];
const Demo = () => {
const [show, setShow] = useState([]);
useEffect(() => {
if(show.length === sections.length) return;
const delay = show.length > 0 ? 300 : 0;
setTimeout(() => setShow(show => [...show, true]), delay)
}, [show]);
return (
<div>{JSON.stringify(show)}</div>
);
};
ReactDOM
.createRoot(root)
.render(<Demo />);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="root"></div>
CodePudding user response:
Solution was setting up an if statement for the first value. Still don't know why useState behaves this way for the initial count.
const [show, setShow] = useState<boolean[]>([]);
useEffect(() => {
async function showSections() {
if (show.length > renderRoutine.sections.length) return;
console.log("SHOW LENGTH: ", show.length, "ROUTINE LENGTH: ", renderRoutine.sections.length);
const delay = show.length > 0 ? 100 : 0;
await timer(delay); // wait if not first
if (show.length === 0) {
setShow([true]);
} else setShow((show) => [...show, true]);
console.log(show);
}
showSections();
}, [show]);