Limpep
Limpep

Reputation: 499

Component router after timeout with state

I currently have a component that does a history.push('/') after a few seconds. But I am getting warnings

index.js:1375 Warning: Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state.

and also

index.js:1375 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

I am fairly new to React, do I need do some sort of clean up?

Here is my component.

import React, {useState} from 'react'
import {UsePostOrPutFetch} from "../hooks/postHook";
import "./styles/ConfirmationChange.scss";
import moment from 'moment';


export default function ConfirmatonChange(props) {

  const [shouldFetch, setShouldFetch] = useState(false);
  const [data,loading,isError, errorMessage] = UsePostOrPutFetch("/send-email/", props.data.location.state.value,"POST", shouldFetch, setShouldFetch);
  const [countDown, setCountDown] = useState(5)

  let spinner = (
    <strong className="c-spinner" role="progressbar">
      Loading…
    </strong>
  );

    const changeView = () => 
    {
      if (countDown < 0) {
        props.data.history.push('/')
      } else {
        setTimeout(() => {
          setCountDown(countDown - 1)
         }
        , 1000)
      }
    }

    return (
        <div>
            <div className="o-container">
                  <article className="c-tile">
                  <div className="c-tile__content">
                    <div className="c-tile__body u-padding-all">

                      <button className = "c-btn c-btn--primary u-margin-right" onClick={props.data.history.goBack}>Edit</button>
                      <button className = "c-btn c-btn--secondary u-margin-right" disabled={loading} onClick={(e) => { setShouldFetch(true)}}>Send</button>
                      {!loading && data === 200 && !isError ? (
                      <div className="ConfirmationChange-success-send">
                       <hr hidden={!data === 200} className="c-divider" />
                        Email Sent succesfully
                        <p>You will be redirected shortly....{countDown}</p>
                          {changeView()}

                      </div>
                      ) : (loading && !isError ? spinner : 
                        <div className="ConfirmationChange-error-send">
                          <hr hidden={!isError} className="c-divider" />
                           {errorMessage}
                        </div>
                      )} 
                    </div>
                  </div>
                </article>
           </div>
        </div>
    )
}

And here is what my data fetch component look like

import { useState, useEffect } from "react";
import { adalApiFetch } from "../config/adal-config";

const UsePostOrPutFetch = (url, sendData, methodType, shouldFetch, setShouldSend) => {

  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [isError, setIsError] = useState(false);
  const [errorMessage, setError] = useState("");

  useEffect(() => {
      const ac = new AbortController();
      if (shouldFetch) {
        const postOrPutData = async () => {
          try {
            const response = await adalApiFetch(fetch, url, 
              {
                signal: ac.signal,
                method: methodType,
                headers: {
                    'Content-Type': 'application/json'
                  },
                  body: JSON.stringify(sendData)
            });
            const json = await response.json();
            setData(await json);
            setLoading(true);
          } catch (err) {
            setIsError(true);
            setError(err.message);
          } finally {
            setShouldSend(false)
            setLoading(false);
          }
      };
      postOrPutData();
    }
      return () => { ac.abort(); };

    }, [shouldFetch, sendData, url, methodType, setShouldSend]);

    return [data, loading, isError, errorMessage];

  };

  export {UsePostOrPutFetch}

Any help would be greatly appreciated.

Upvotes: 2

Views: 491

Answers (2)

johnny peter
johnny peter

Reputation: 4872

Yup, you have a timeOut that could potentially fire after the component has unmounted.

You need to add a useEffect that clears the timer onUnmount as follows

const timerRef = useRef();
useEffect(() => () => clearTimeout(timerRef.current), [])

const changeView = () => {
  if (countDown < 0) {
    props.data.history.push("/");
  } else {
    timerRef.current = setTimeout(() => {
      setCountDown(countDown - 1);
    }, 1000);
  }
};

Upvotes: 1

Taki
Taki

Reputation: 17654

Check React Hooks - Check If A Component Is Mounted

The most common cause of this warning is when a user kicks off an asynchronous request, but leaves the page before it completes.

You'll need a componentIsMounted variable and useEffect and useRef hooks :

const componentIsMounted = useRef(true);
useEffect(() => {
  return () => {
    componentIsMounted.current = false;
  };
}, []);

const changeView = () => {
  if (countDown < 0) {
    props.data.history.push("/");
  } else {
    setTimeout(() => {
      if (componentIsMounted.current) { // only update the state if the component is mounted
        setCountDown(countDown - 1);
      }
    }, 1000);
  }
};

You should do the same for data fetch component

Upvotes: 1

Related Questions