Home > front end >  My for loop in useEffect adds value too quickly
My for loop in useEffect adds value too quickly

Time:01-22

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. screenshot of console

vid

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]);
  • Related