Search code examples
reactjsionic-frameworkuse-effect

Can't perform a react state update on an unmounted component error


I'll shortly preface by saying I've been at this for hours and feel like I've tried everything out there. I also left out a lot of code from my component, that I believe is unrelated to the issue. The user will see this component, EditEpic, when they are trying to edit an epic object (entry in db). Here the user can also delete the object from the db which is what I am trying to do now. After deletion, the user will be taken back to the previous page. That is where I am getting the issue. Here is the code

interface IEditEpic extends RouteComponentProps<{
    id: string;
}> {
}

interface IProps {
    updater?: (val:number) => void;
}

export const EditEpic: React.FC<IEditEpic> = ({match}, props:IProps) => {
    const [showAlert2, setShowAlert2] = useState(false);
    const [showAlert3, setShowAlert3] = useState(false);
    const [showAlert4, setShowAlert4] = useState(false);
    const [showAlert5, setShowAlert5] = useState(false);
    const [showAlert6, setShowAlert6] = useState(false);
    const [showAlert7, setShowAlert7] = useState(false);
    const [showAlert8, setShowAlert8] = useState(false);
    const [showAlert9, setShowAlert9] = useState(false);
    const [showAlert10, setShowAlert10] = useState(false);
    const [showAlert11, setShowAlert11] = useState(false);
    const [showAlert12, setShowAlert12] = useState(false);
    const [resultMessage, setResultMessage] = useState();
    const [epic, setEpic] = useState<any>();
    const [pet, setPet] = useState({petPoints: 0, id: ""});
    const history = useHistory();
    const [triggerRefresh, setTriggerFresh] = useState(0);

    useEffect(() => {
        let isSubscribed:boolean = true;
        if (isSubscribed) {
            (async () => {
                const res = await findEpic(match.params.id);
                if (res){
                    console.log("found epic ", res[0]);
                    setEpic(res[0]);
                }
                // set pet
                var my_pet:any = await getPet();
                if (my_pet)
                    if (my_pet.length > 0) {
                        setPet({petPoints: my_pet[0].points, id: my_pet[0]._id});
                    }
            })();
        }
        return () => {isSubscribed = false};
    }, [pet.petPoints]);


    const removeEpic = () => {
        (async () => {
            const result = await deleteEpic(epic);
            if (result === DELETE_EPIC_RESULT.pass)
            {
                console.log("Successful delete");
                setShowAlert7(true);
                console.log("going back in history");
                history.goBack();
            }
            else if (result === DELETE_GOALS_IN_EPIC_RESULT.fail)
                setShowAlert11(true);
            else if (result === DELETE_EPIC_RESULT.id_error)
                setShowAlert12(true);
            else
                setShowAlert6(true);
        })();
    };

    console.log("MY props updater", props.updater);

    return (
        <IonPage>
            <IonContent>
                <IonHeader>
                    <IonToolbar>
                        <IonButtons slot="start">
                            <IonBackButton defaultHref="/home" icon="buttonIcon"/>
                        </IonButtons>
                    </IonToolbar>
                </IonHeader>
                <IonList>
                    <div style={{display: "flex", justifyContent: "center"}}>
                        <h1 style={{fontWeight: "bold", textDecoration: "underline"}}>Edit Epic</h1>
                    </div>
                    <br/>
                    <IonItem>
                        <IonInput
                            value={epic?.epicTitle}
                            placeholder="Title"
                            required={true}
                            debounce={750}
                            clearInput={true}
                            minlength={1}
                            maxlength={50}
                            // onIonChange={e => setTitle(e.detail.value!)}
                            onIonChange={e => setEpic({...epic, epicTitle: e.detail.value!})}
                        >
                        </IonInput>
                    </IonItem>
                    <br/>
                    <br/>
                    <br/>
                    <IonItem>
                        <IonLabel position="floating">Description</IonLabel>
                        <IonTextarea
                            value={epic?.epicDescription}
                            placeholder="Please enter your description here"
                            onIonChange={e => setEpic({...epic, epicDescription: e.detail.value!})}>
                        </IonTextarea>
                    </IonItem>
                    <br/>
                    <br/>
                    <DateTimePicker dateType={DATE_ENUMS.start} setDate={setEpic} goalState={epic}/>
                    <br/>
                    <br/>
                    <DateTimePicker dateType={DATE_ENUMS.end} setDate={setEpic} goalState={epic}/>
                    <br/>
                    <br/>
                    <IonFab horizontal="end" vertical="top" slot="fixed">
                        <IonFabButton color="danger" onClick={() => setShowAlert5(true)}>
                            <IonIcon icon={trashOutline}/>
                        </IonFabButton>
                    </IonFab>
                    <IonAlert
                        isOpen={showAlert6}
                        onDidDismiss={() => setShowAlert6(false)}
                        header={'Error'}
                        message={DELETE_EPIC_RESULT.error}
                        buttons={["OK"]}
                    />
                    <IonAlert
                        isOpen={showAlert7}
                        onDidDismiss={() => setShowAlert7(false)}
                        header={'Success'}
                        message={DELETE_EPIC_RESULT.pass}
                        buttons={["OK"]}
                    />
                    <IonAlert
                        isOpen={showAlert11}
                        onDidDismiss={() => setShowAlert11(false)}
                        header={'Error'}
                        message={DELETE_GOALS_IN_EPIC_RESULT.fail}
                        buttons={["OK"]}
                    />
                    <IonAlert
                        isOpen={showAlert12}
                        onDidDismiss={() => {
                            setShowAlert12(false);
                            setTriggerFresh(triggerRefresh+ 1);
                        }}
                        header={'Error'}
                        message={DELETE_EPIC_RESULT.id_error}
                        buttons={["OK"]}
                    />
                    <IonAlert
                        isOpen={showAlert5}
                        onDidDismiss={() => {
                            setShowAlert5(false);
                            // close modal if the insert was successful
                        }}
                        header={'Delete Epic'}
                        message={"Are you sure that you would like to delete the selected epic? " +
                        "Goals associated with the epic will be deleted as well."}
                        buttons={[{
                            text: "NO",
                            handler: () => {
                                // do nothing
                            }
                        }, {
                            text: "YES",
                            handler: () => {
                                removeEpic();
                            }
                        }]}
                    />
                </IonList>
            </IonContent>
        </IonPage>
    )
};

export default EditEpic;

What's important here is the very last IonAlert tag in what is being returned by my component. In the button handler for the alert, I run the function removeEpic() if the user clicks "YES". This function deletes the epic object in the database and what I want to happen is go back to the last page in history, since the user would have just deleted what they were looking at. When I have history.goBack() commented out, the code runs fine.

I'm was thinking the issue could be because history.goBack() was called inside an async function, perhaps since that function was still in progress, and I was going back a page, react would give me that error. However, commenting out goBack() from removeEpic() and placing it after my removeEpic() call in the IonAlert didn't work either. I'm not sure what else to try other than that. I tried some cleanup stuff in useEffect as well but nothing worked. Any help would be appreciated. I can supply more info if needed.

Also, my stack trace looks like so

index.js:1 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.
    in EditEpic (created by Context.Consumer)
    in Route (at App.tsx:66)
    in View (created by StackManagerInner)

Solution

  • Here is what I would do:

    1. add const isMounted = useRef(true) on top of your EditEpic component
    2. have another (additional) useEffect which will look like this:
    useEffect(() => { return () => { isMounted.current = false} }, [])
    
    1. go threw every line of code where you have setSomething.. and add check isMounted.current && setSomething..

    Idea is that with useRef you are keeping track is component mounted or not and than updating state only if component is mounted.