Or Nakash
Or Nakash

Reputation: 3329

React-Native useEffect cleanup

I am trying to fetch data from API - it usually works but sometimes I get 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.

My useEffect

useEffect(() => {
    let abortController = new AbortController();
    getAccountInformation(abortController);

    return () => { 
      abortController.abort();
     };
  }, [isFocused]);

The function

async function getAccountInformation(abortController) {
    try {
      const token = await AsyncStorage.getItem("token");
      const client_id = await AsyncStorage.getItem("client_id");
      await fetch(API_ADD + "/getClientInformation/" + client_id, {
        signal: abortController.signal,
        method: "GET",
        headers: {
          Authorization: "Bearer " + token,
          "Content-Type": "application/json",
        },
      })
        .then((response) => response.text())
        .then((responseJson) => {
          const safeResponse = responseJson.length
            ? JSON.parse(responseJson)
            : {};

          setClientInformation(safeResponse);
          getBookingsByClientId(abortController);
        }).catch(err=>{
          if(err.name==='AbortError')
          {
            console.log("Fetch abort - caught an error");
          }
          else 
          {
            console.log(err.message);
            Alert.alert(err.message);
          }
        })
        .done();
    } catch (e) {
      console.log(e);
    }
  }

Unfortunately I couldn't find any solution for this - only solutions for a function inside useEffect

Upvotes: 0

Views: 177

Answers (1)

Dmitriy Mozgovoy
Dmitriy Mozgovoy

Reputation: 1597

AbortController can reject only the first 'fetch' promise. Just check signal.aborted before changing the state.

async function getAccountInformation(abortController) {
    try {
      const token = await AsyncStorage.getItem("token");
      const client_id = await AsyncStorage.getItem("client_id");
      const {signal}= abortController;
      await fetch(API_ADD + "/getClientInformation/" + client_id, {
        signal,
        method: "GET",
        headers: {
          Authorization: "Bearer " + token,
          "Content-Type": "application/json",
        },
      })
        .then((response) => response.text())
        .then((responseJson) => {
          const safeResponse = responseJson.length
            ? JSON.parse(responseJson)
            : {};
          if(!signal.aborted){ // do not change the state if controller has been aborted
            setClientInformation(safeResponse);
            getBookingsByClientId(abortController);
          }
        }).catch(err=>{
          if(err.name==='AbortError')
          {
            console.log("Fetch abort - caught an error");
          }
          else 
          {
            console.log(err.message);
            Alert.alert(err.message);
          }
        })
        .done();
    } catch (e) {
      console.log(e);
    }
  }

Ideally, you should interrupt the function as soon as possible after unmounting the component to avoid doing unnecessary work. But in a minimal solution, you just need to avoid state changes.

Or you can check out this demo with custom hooks usage, that takes care of cancellation&request aborting on unmounting (or if requested by user) automatically:

import React, { useState } from "react";
import {
  useAsyncCallback,
  CanceledError,
  E_REASON_UNMOUNTED
} from "use-async-effect2";
import cpFetch from "cp-fetch";

export default function TestComponent(props) {
  const [text, setText] = useState("");

  const fetchCharacter = useAsyncCallback(
    function* (id) {
      const response = yield cpFetch(
        `https://rickandmortyapi.com/api/character/${id}`
      );
      return yield response.json();
    }
  );

  const fetchUrl = useAsyncCallback(
    function* () {
      this.timeout(props.timeout);
      try {
        setText("fetching...");
        const response = yield cpFetch(props.url);
        const json = yield response.json();
        const character = yield fetchCharacter(Math.round(Math.random() * 100));
        setText(
          JSON.stringify(
            {
              firstResponse: json,
              character
            },
            null,
            2
          )
        );
      } catch (err) {
        CanceledError.rethrow(err, E_REASON_UNMOUNTED);
        setText(err.toString());
      }
    },
    [props.url, props.timeout]
  );

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <div>{text}</div>
      <button className="btn btn-success" onClick={() => fetchUrl(props.url)}>
        Fetch data
      </button>
      <button className="btn btn-warning" onClick={() => fetchUrl.cancel()}>
        Cancel request
      </button>
    </div>
  );
}

Upvotes: 1

Related Questions