I update state inside useEffect and expect webpage to rerender on every state update

I update state inside useEffect and expect webpage to rerender on every state update but it seems like it only updates once - the last value from for loop. And despite me setting timeout the update and rerender happens instantly. What am I doing wrong?

const [definition, setDefinition] = useState('')

    useEffect(() => {
        if (startAnimationFinished) return
        if (!pathIsActive) return

        let newDef
        for (let i = 0; i < 150; i++) {
            newDef = `M ${i} ${i} L ${i} ${i}`
            setTimeout(() => {
                setDefinition(newDef)
            }, 1000)
        }

        console.log(newDef);

        startAnimationFinished = true
    }, [pathIsActive])

    return (
        <>
            <svg
                className={'path' + (pathIsActive ? ' active' : '')}
                xmlns="http://www.w3.org/2000/svg"
                style={{ border: '1px solid blue' }}
                viewBox={`${-width / 4} ${-width / 4} ${width} ${width}`}
                {...props}
            >
                <path fill='none' stroke="white" width='1'
                    style={{ filter: "drop-shadow(0px 0px 5px white)" }}
                    d={definition}
                />
            </svg>
        </>

Upvotes: 1

Views: 144

Answers (3)

Anton Podolsky
Anton Podolsky

Reputation: 850

  1. React state updates are batched, meaning multiple subsequent updates can trigger only one re-render
  2. You are effectively scheduling all setTimeouts to run at the same time
  3. Because of the inherent behaviour of closures, all the setTimeouts will see the same value of the newDef variable (which will be the last assigned value).

If you want to update the state in 1 second intervals you can do something like this:

const updateEverySecond = async () => {
  let i = 0;

  while (i++ < 150) {
     const newDef = `M ${i} ${i} L ${i} ${i}`;

     setDefinition(newDef);

     await new Promise(resolve => setTimeout(resolve, 1000));
  }
}

useEffect(() => {
  updateEverySecond();
}, []);

Upvotes: 1

Mallikarjun M G
Mallikarjun M G

Reputation: 609

If you want start animation per sec use setInterval instead of setTimeOut

import React, { useState } from "react";

export default function App() {
  const [definition, setDefinition] = React.useState("");
  const [secs, setSecs] = useState(0);
  const [pathIsActive, setActivePath] = useState(false);
  React.useEffect(() => {
    let timerId;
    let startAnimationFinished = false;
    const startAnim = () => {
      if (!startAnimationFinished && !pathIsActive) {
        let newDef;
        timerId = setInterval(() => {
          newDef = `M ${secs} ${secs} L ${secs} ${secs}`;
          setDefinition(newDef);
          setSecs((pre) => pre + 1);
        }, 1000);
      }
    };
    const stopAnim = () => {
      clearInterval(timerId);
      startAnimationFinished = true;
    };
    if (secs <= 150) {
      startAnim();
    } else {
      stopAnim();
    }
    return () => {
      clearInterval(timerId);
      startAnimationFinished = false;
    };
  }, [secs, pathIsActive]);
  return (
    <>
      <svg
        className={"path" + (pathIsActive ? " active" : "")}
        xmlns="http://www.w3.org/2000/svg"
        style={{ border: "1px solid blue" }}
        viewBox={`${-width / 4} ${-width / 4} ${width} ${width}`}
        {...props}
      >
        <path
          fill="none"
          stroke="white"
          width="1"
          style={{ filter: "drop-shadow(0px 0px 5px white)" }}
          d={definition}
        />
      </svg>
    </>
  );
}

Upvotes: 1

Ahmed Sbai
Ahmed Sbai

Reputation: 16189

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.

consider updating multiple hooks in your useEffect, this will not be good behavior if the component will be rendered for every update. so once useEffect ends the component re render.

here if you want to store all the values without loosing any, because here you are overwritting the value of definition in every itteration you can declare definition as array

const [definition, setDefinition] = useState([]);

then update it like this :

let newDef = [];
for (let i = 0; i < 150; i++) {
  newDef.push(`M ${i} ${i} L ${i} ${i}`)
}
setDefinition(newDef)

now you have the list of definitions you can display them as you want

Upvotes: 1

Related Questions