Abhay Sehgal
Abhay Sehgal

Reputation: 1723

React Warning: Cannot update during an existing state transition (such as within `render`)

I've ResetPassword component which renders Timer component, below are their code -

ResendPassword.js

class ResetPassword extends Component{
    constructor(props){
        super(props);
        this.state = {
            resendActive: false
        };
    }

    endHandler(){
        this.setState({
            resendActive: true
        })
    }        

    render(){
        return (
            <Timer sec={5} counter={this.state.counter} end={this.endHandler.bind(this)}/>
        )
    }
}

Timer.js

const Timer = (props) => {
    const [sec, setSec] = useState(props.sec);

    useEffect(() => {
        setSec(props.sec);
        const intr = setInterval(() => {
            setSec((s) => {
                if(s > 0)
                    return --s;
                props.end(); // Line: causing warning
                clearInterval(intr);
                return s;
            });
        }, 1000)
    
        return () => {
            clearInterval(intr);
        }
    }, [props.counter])

    return (
        <span>{sec > 60 ? `${Math.floor(sec/60)}:${sec - Math.floor(sec/60)}`: `${sec}`} sec</span>
    )
}

In Above code I'm using timer in ResetPassword and I want a function call when timer ends so I'm passing endHandler as end in Timer component but calling that function giving - Warning: Cannot update during an existing state transition (such as within 'render'), can anyone let me know what I'm doing wrong here?

Thanks In Advance

Upvotes: 1

Views: 508

Answers (1)

Drew Reese
Drew Reese

Reputation: 202836

Issue

setSec is a state update function and you use the functional state update variant. This update function callback is necessarily required to be a pure function, i.e. with zero side-effects. The invocation of props.end() is a side-effect.

Solution

Split out the side-effect invocation of props.end into its own effect hook so that it is independent of the state updater function.

const Timer = (props) => {
  const [sec, setSec] = useState(props.sec);

  useEffect(() => {
    setSec(props.sec);
    const intr = setInterval(() => {
      setSec((s) => {
        if (s > 0) return --s;
        clearInterval(intr);
        return s;
      });
    }, 1000);

    return () => {
      clearInterval(intr);
    };
  }, [props.counter]);

  useEffect(() => {
    console.log(sec);
    if (sec <= 0) props.end(); // <-- move invoking `end` to own effect
  }, [sec]);

  return (
    <span>
      {sec > 60
        ? `${Math.floor(sec / 60)}:${sec - Math.floor(sec / 60)}`
        : `${sec}`}{" "}
      sec
    </span>
  );
};

Edit react-warning-cannot-update-during-an-existing-state-transition-such-as-within issue

Suggestion

Create a useInterval hook

const useInterval = (callback, delay) => {
  const savedCallback = useRef(null);

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    const id = setInterval(savedCallback.current, delay);
    return () => clearInterval(id);
  }, [delay]);
};

Update Timer to use interval hook

const Timer = ({ end, sec: secProp}) => {
  const [sec, setSec] = useState(secProp);

  // Only decrement sec if sec !== 0
  useInterval(() => setSec((s) => s - (s ? 1 : 0)), 1000);

  useEffect(() => {
    !sec && end(); // sec === 0, end!
  }, [sec, end]);

  return (
    <span>
      {sec > 60
        ? `${Math.floor(sec / 60)}:${sec - Math.floor(sec / 60)}`
        : `${sec}`}{" "}
      sec
    </span>
  );
};

Edit react-warning-cannot-update-during-an-existing-state-transition-such-as-within

Upvotes: 2

Related Questions