gmacerola
gmacerola

Reputation: 57

React state update memory leak due to an unmounted component

My app uses firebase to authenticate. During the sign-in process, I get the "Can't perform a React state update on an unmounted component" and it recommends using a cleanup function in a useEffect. I thought I was cleaning up the function in async function with the

finally {
      setLoading(false);
    }

Any help would be appreciated. Code below:

import React, { useState, useContext } from "react";
import styled from "styled-components/native";
import { Image, Text, StyleSheet } from "react-native";

import { FirebaseContext } from "../context/FirebaseContext";
import { UserContext } from "../context/UserContext";

export default function SignInScreen() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);
  const firebase = useContext(FirebaseContext);
  const [_, setUser] = useContext(UserContext);

  const signIn = async () => {
    setLoading(true);
    try {
      await firebase.signIn(email, password);
      const uid = firebase.getCurrentUser().uid;
      const userInfo = await firebase.getUserInfo(uid);
      const emailArr = userInfo.email.split("@");
      setUser({
        username: emailArr[0],
        email: userInfo.email,
        uid,
        isLoggedIn: true,
      });
    } catch (error) {
      alert(error.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <Container>
      <Main>
        <Text style={styles.welcomeText}>Welcome</Text>
      </Main>

      <Auth>
        <AuthContainer>
          <AuthTitle>Email Address</AuthTitle>
          <AuthField
            autoCapitalize="none"
            autoCompleteType="email"
            autoCorrect={false}
            autoFocus={true}
            keyboardType="email-address"
            onChangeText={(email) => setEmail(email.trim())}
            value={email}
          />
        </AuthContainer>

        <AuthContainer>
          <AuthTitle>Password</AuthTitle>
          <AuthField
            autoCapitalize="none"
            autoCompleteType="password"
            autoCorrect={false}
            autoFocus={true}
            secureTextEntry={true}
            onChangeText={(password) => setPassword(password.trim())}
            value={password}
          />
        </AuthContainer>
      </Auth>

      <SignInContainer onPress={signIn} disabled={loading}>
        {loading ? <Loading /> : <Text style={styles.text}>Sign In</Text>}
      </SignInContainer>
      <HeaderGraphic>
        <Image
          source={require("../images/heritage-films-logo.png")}
          style={{ height: 150, width: 300, resizeMode: "contain" }}
        />
      </HeaderGraphic>
    </Container>
  );
}

Upvotes: 1

Views: 465

Answers (1)

Dmitriy Mozgovoy
Dmitriy Mozgovoy

Reputation: 1597

You should check if the component is still mounted before calling setState in some way. It's a typical React leakage issue. You can implement isMounted variable with useRef hook for that, despite the fact that the authors of React call it an anti-pattern, since you should cancel your async routines when the component unmounts.

function Component() {
    const isMounted = React.useRef(true);

    React.useEffect(() => () => (isMounted.current = false), []);

      const signIn = async () => {
        setLoading(true);
        try {
          await firebase.signIn(email, password);
          const uid = firebase.getCurrentUser().uid;
          const userInfo = await firebase.getUserInfo(uid);
          const emailArr = userInfo.email.split("@");
          isMounted.current && setUser({
            username: emailArr[0],
            email: userInfo.email,
            uid,
            isLoggedIn: true,
          });
        } catch (error) {
          alert(error.message);
        } finally {
          isMounted.current && setLoading(false);
        }
   };
}

Or another a bit magic way:

import { useAsyncCallback, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CanceledError } from "c-promise2";

export default function SignInScreen() {
  //...

  const signIn = useAsyncCallback(function*() {
    setLoading(true);
    try {
      yield firebase.signIn(email, password);
      const uid = firebase.getCurrentUser().uid;
      const userInfo = yield firebase.getUserInfo(uid);
      const emailArr = userInfo.email.split("@");
      setUser({
        username: emailArr[0],
        email: userInfo.email,
        uid,
        isLoggedIn: true,
      });
      setLoading(false);
    } catch (error) {
      CanceledError.rethrow(error, E_REASON_UNMOUNTED);
      setLoading(false);
      alert(error.message);
    }
  }, []);

  return (<YourJSX onPress={signIn}>);
}

Upvotes: 2

Related Questions