How to cancel a fetch on componentWillUnmount
Asked Answered
T

16

116

I think the title says it all. The yellow warning is displayed every time I unmount a component that is still fetching.

Console

Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but ... To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.

  constructor(props){
    super(props);
    this.state = {
      isLoading: true,
      dataSource: [{
        name: 'loading...',
        id: 'loading',
      }]
    }
  }

  componentDidMount(){
    return fetch('LINK HERE')
      .then((response) => response.json())
      .then((responseJson) => {
        this.setState({
          isLoading: false,
          dataSource: responseJson,
        }, function(){
        });
      })
      .catch((error) =>{
        console.error(error);
      });
  }
Tub answered 18/4, 2018 at 18:15 Comment(5)
what is it warning i don't have that issueEdington
question updatedAsmara
did you promise or async code for fetchEdington
add you fetch code to qustionEdington
see isMounted is an Antipattern and aborting a fetch.Dawnedawson
G
119

When you fire a Promise it might take a few seconds before it resolves and by that time user might have navigated to another place in your app. So when Promise resolves setState is executed on unmounted component and you get an error - just like in your case. This may also cause memory leaks.

That's why it is best to move some of your asynchronous logic out of components.

Otherwise, you will need to somehow cancel your Promise. Alternatively - as a last resort technique (it's an antipattern) - you can keep a variable to check whether the component is still mounted:

componentDidMount(){
  this.mounted = true;

  this.props.fetchData().then((response) => {
    if(this.mounted) {
      this.setState({ data: response })
    }
  })
}

componentWillUnmount(){
  this.mounted = false;
}

I will stress that again - this is an antipattern but may be sufficient in your case (just like they did with Formik implementation).

A similar discussion on GitHub

EDIT:

This is probably how would I solve the same problem (having nothing but React) with Hooks:

OPTION A:

import React, { useState, useEffect } from "react";

export default function Page() {
  const value = usePromise("https://something.com/api/");
  return (
    <p>{value ? value : "fetching data..."}</p>
  );
}

function usePromise(url) {
  const [value, setState] = useState(null);

  useEffect(() => {
    let isMounted = true; // track whether component is mounted

    request.get(url)
      .then(result => {
        if (isMounted) {
          setState(result);
        }
      });

    return () => {
      // clean up
      isMounted = false;
    };
  }, []); // only on "didMount"

  return value;
}

OPTION B: Alternatively with useRef which behaves like a static property of a class which means it doesn't make component rerender when it's value changes:

function usePromise2(url) {
  const isMounted = React.useRef(true)
  const [value, setState] = useState(null);


  useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  useEffect(() => {
    request.get(url)
      .then(result => {
        if (isMounted.current) {
          setState(result);
        }
      });
  }, []);

  return value;
}

// or extract it to custom hook:
function useIsMounted() {
  const isMounted = React.useRef(true)

  useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  return isMounted; // returning "isMounted.current" wouldn't work because we would return unmutable primitive
}

Example: https://codesandbox.io/s/86n1wq2z8

Gluck answered 18/4, 2018 at 18:29 Comment(17)
so there's no real way to just cancel the fetch on the componentWillUnmount ?Asmara
@JoãoBelo I'm not sure if it's supported. Take a look at this packageGluck
Oh, I didn't notice the code of your answer before, it did work. thanksAsmara
see isMounted is an Antipattern and aborting a fetch.Dawnedawson
It's now necessary to name the "isMounted" variable differently, otherwise you'll get an error setting getter-only property Urquhart
what do you mean by "That's why it is best to move your asynchronous logic out of components."? Isn't everything in react a component?Korman
@Korman i mean using redux or mobx or other state management library. However new coming features like react-suspense may solve it.Gluck
@Tomasz Mularczyk Thank you so much, you did worthy stuff.Reinhold
Also see the React Hooks FAQ for an example.Exquisite
Using a control variable such as this.mounted you used is exactly what I was thinking as the 1st solution. I am actually searching for other solutions, but since you mentioned it at the 1st time, I think I will use it. Thanks for indirect confirmation to the same idea as mine.Wailoo
Is the React Hooks solution any better than the first solution? It uses a fancy new feature but it basically works out the same.Jeaniejeanine
Warning: isMounted(...) is deprecated in plain JavaScript React classes.Burschenschaft
aside from being an anti-pattern, the first solution does not work because setState is ASYNCHRONOUSNonflammable
Read the anti-pattern link more carefully. Using the method isMounted() is an anti-pattern, but using a property, like _isMounted is recommended.Frankfurter
@haleonj, True, i noticed it tooPortray
I think this is against rule of hooks in the sense that you're setting state inside conditional statementConduplicate
@Conduplicate you can conditionally call the setState function returned by useState (and you wouldn't be able to do much with react if you couldn't), but you can't conditionally call useState (or any other hook). Hooks are effectful and React depends on their call order to resolve state, so if a hook is conditionally rendered the call order will change and it will break. See reactjs.org/docs/hooks-rules.html#explanation for details.Avaavadavat
F
31

The friendly people at React recommend wrapping your fetch calls/promises in a cancelable promise. While there is no recommendation in that documentation to keep the code separate from the class or function with the fetch, this seems advisable because other classes and functions are likely to need this functionality, code duplication is an anti-pattern, and regardless the lingering code should be disposed of or canceled in componentWillUnmount(). As per React, you can call cancel() on the wrapped promise in componentWillUnmount to avoid setting state on an unmounted component.

The provided code would look something like these code snippets if we use React as a guide:

const makeCancelable = (promise) => {
    let hasCanceled_ = false;

    const wrappedPromise = new Promise((resolve, reject) => {
        promise.then(
            val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
            error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
        );
    });

    return {
        promise: wrappedPromise,
        cancel() {
            hasCanceled_ = true;
        },
    };
};

const cancelablePromise = makeCancelable(fetch('LINK HERE'));

constructor(props){
    super(props);
    this.state = {
        isLoading: true,
        dataSource: [{
            name: 'loading...',
            id: 'loading',
        }]
    }
}

componentDidMount(){
    cancelablePromise.
        .then((response) => response.json())
        .then((responseJson) => {
            this.setState({
                isLoading: false,
                dataSource: responseJson,
            }, () => {

            });
        })
        .catch((error) =>{
            console.error(error);
        });
}

componentWillUnmount() {
    cancelablePromise.cancel();
}

---- EDIT ----

I have found the given answer may not be quite correct by following the issue on GitHub. Here is one version that I use which works for my purposes:

export const makeCancelableFunction = (fn) => {
    let hasCanceled = false;

    return {
        promise: (val) => new Promise((resolve, reject) => {
            if (hasCanceled) {
                fn = null;
            } else {
                fn(val);
                resolve(val);
            }
        }),
        cancel() {
            hasCanceled = true;
        }
    };
};

The idea was to help the garbage collector free up memory by making the function or whatever you use null.

Frankfurter answered 7/9, 2018 at 15:53 Comment(4)
do you have the link to the issue on githubIphigeniah
@Ren, there is a GitHub site for editing the page and discussing issues.Frankfurter
I'm no longer sure where the exact issue is on that GitHub project.Frankfurter
Link to the GitHub issue: github.com/facebook/react/issues/5465Alongshore
B
28

You can use AbortController to cancel a fetch request.

See also: https://www.npmjs.com/package/abortcontroller-polyfill

class FetchComponent extends React.Component{
  state = { todos: [] };
  
  controller = new AbortController();
  
  componentDidMount(){
    fetch('https://jsonplaceholder.typicode.com/todos',{
      signal: this.controller.signal
    })
    .then(res => res.json())
    .then(todos => this.setState({ todos }))
    .catch(e => alert(e.message));
  }
  
  componentWillUnmount(){
    this.controller.abort();
  }
  
  render(){
    return null;
  }
}

class App extends React.Component{
  state = { fetch: true };
  
  componentDidMount(){
    this.setState({ fetch: false });
  }
  
  render(){
    return this.state.fetch && <FetchComponent/>
  }
}

ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Boost answered 22/11, 2018 at 17:37 Comment(3)
I wish I had known that there is a Web API for canceling requests like AbortController. But alright, it's not too late to know it. Thank you.Wailoo
So if you have multiple fetches, can you pass that single AbortController to all of them?Thirza
perhaps, each of .then() should include the check as well: if (this.controller.signal.abored) return Promise.reject('Aborted');Whirl
I
16

Since the post had been opened, an "abortable-fetch" has been added. https://developers.google.com/web/updates/2017/09/abortable-fetch

(from the docs:)

The controller + signal manoeuvre Meet the AbortController and AbortSignal:

const controller = new AbortController();
const signal = controller.signal;

The controller only has one method:

controller.abort(); When you do this, it notifies the signal:

signal.addEventListener('abort', () => {
  // Logs true:
  console.log(signal.aborted);
});

This API is provided by the DOM standard, and that's the entire API. It's deliberately generic so it can be used by other web standards and JavaScript libraries.

for example, here's how you'd make a fetch timeout after 5 seconds:

const controller = new AbortController();
const signal = controller.signal;

setTimeout(() => controller.abort(), 5000);

fetch(url, { signal }).then(response => {
  return response.text();
}).then(text => {
  console.log(text);
});
Intrados answered 26/12, 2018 at 15:15 Comment(5)
Interesting, I will try this way. But prior to that, I will read the AbortController API first.Wailoo
Can we use just one AbortController instance for multiple fetches such that when we invoke the abort method of this single AbortController in the componentWillUnmount, it will cancel all of the existing fetches in our component ? If not, it means we have to provide different AbortController instances for each of the fetches, right ?Wailoo
@LexSoft did you find an answer to your question?Tribromoethanol
@Superdude the answer is yesShirley
You can use just one instance of AbortController for multiple fetches, but it will cancel the requests simultaneously, otherwise, you need to create separate instances.Deferent
T
2

When I need to "cancel all subscriptions and asynchronous" I usually dispatch something to redux in componentWillUnmount to inform all other subscribers and send one more request about cancellation to server if necessary

Tenebrous answered 18/4, 2018 at 18:40 Comment(0)
I
2

The crux of this warning is that your component has a reference to it that is held by some outstanding callback/promise.

To avoid the antipattern of keeping your isMounted state around (which keeps your component alive) as was done in the second pattern, the react website suggests using an optional promise; however that code also appears to keep your object alive.

Instead, I've done it by using a closure with a nested bound function to setState.

Here's my constructor(typescript)…

constructor(props: any, context?: any) {
    super(props, context);

    let cancellable = {
        // it's important that this is one level down, so we can drop the
        // reference to the entire object by setting it to undefined.
        setState: this.setState.bind(this)
    };

    this.componentDidMount = async () => {
        let result = await fetch(…);            
        // ideally we'd like optional chaining
        // cancellable.setState?.({ url: result || '' });
        cancellable.setState && cancellable.setState({ url: result || '' });
    }

    this.componentWillUnmount = () => {
        cancellable.setState = undefined; // drop all references.
    }
}
Inocenciainoculable answered 3/5, 2018 at 11:31 Comment(1)
This is conceptually no different than keeping an isMounted flag, only you're binding it to the closure instead of hanging it of thisBeaverbrook
T
2

I think if it is not necessary to inform server about cancellation - best approach is just to use async/await syntax (if it is available).

constructor(props){
  super(props);
  this.state = {
    isLoading: true,
    dataSource: [{
      name: 'loading...',
      id: 'loading',
    }]
  }
}

async componentDidMount() {
  try {
    const responseJson = await fetch('LINK HERE')
      .then((response) => response.json());

    this.setState({
      isLoading: false,
      dataSource: responseJson,
    }
  } catch {
    console.error(error);
  }
}
Tenebrous answered 29/10, 2019 at 16:20 Comment(0)
S
0

In addition to the cancellable promise hooks examples in the accepted solution, it can be handy to have a useAsyncCallback hook wrapping a request callback and returning a cancellable promise. The idea is the same, but with a hook working just like a regular useCallback. Here is an example of implementation:

function useAsyncCallback<T, U extends (...args: any[]) => Promise<T>>(callback: U, dependencies: any[]) {
  const isMounted = useRef(true)

  useEffect(() => {
    return () => {
      isMounted.current = false
    }
  }, [])

  const cb = useCallback(callback, dependencies)

  const cancellableCallback = useCallback(
    (...args: any[]) =>
      new Promise<T>((resolve, reject) => {
        cb(...args).then(
          value => (isMounted.current ? resolve(value) : reject({ isCanceled: true })),
          error => (isMounted.current ? reject(error) : reject({ isCanceled: true }))
        )
      }),
    [cb]
  )

  return cancellableCallback
}
Silicic answered 2/12, 2019 at 8:37 Comment(0)
Z
0

Using CPromise package, you can cancel your promise chains, including nested ones. It supports AbortController and generators as a replacement for ECMA async functions. Using CPromise decorators, you can easily manage your async tasks, making them cancellable.

Decorators usage Live Demo :

import React from "react";
import { ReactComponent, timeout } from "c-promise2";
import cpFetch from "cp-fetch";

@ReactComponent
class TestComponent extends React.Component {
  state = {
    text: "fetching..."
  };

  @timeout(5000)
  *componentDidMount() {
    console.log("mounted");
    const response = yield cpFetch(this.props.url);
    this.setState({ text: `json: ${yield response.text()}` });
  }

  render() {
    return <div>{this.state.text}</div>;
  }

  componentWillUnmount() {
    console.log("unmounted");
  }
}

All stages there are completely cancelable/abortable. Here is an example of using it with React Live Demo

import React, { Component } from "react";
import {
  CPromise,
  CanceledError,
  ReactComponent,
  E_REASON_UNMOUNTED,
  listen,
  cancel
} from "c-promise2";
import cpAxios from "cp-axios";

@ReactComponent
class TestComponent extends Component {
  state = {
    text: ""
  };

  *componentDidMount(scope) {
    console.log("mount");
    scope.onCancel((err) => console.log(`Cancel: ${err}`));
    yield CPromise.delay(3000);
  }

  @listen
  *fetch() {
    this.setState({ text: "fetching..." });
    try {
      const response = yield cpAxios(this.props.url).timeout(
        this.props.timeout
      );
      this.setState({ text: JSON.stringify(response.data, null, 2) });
    } catch (err) {
      CanceledError.rethrow(err, E_REASON_UNMOUNTED);
      this.setState({ text: err.toString() });
    }
  }

  *componentWillUnmount() {
    console.log("unmount");
  }

  render() {
    return (
      <div className="component">
        <div className="caption">useAsyncEffect demo:</div>
        <div>{this.state.text}</div>
        <button
          className="btn btn-success"
          type="submit"
          onClick={() => this.fetch(Math.round(Math.random() * 200))}
        >
          Fetch random character info
        </button>
        <button
          className="btn btn-warning"
          onClick={() => cancel.call(this, "oops!")}
        >
          Cancel request
        </button>
      </div>
    );
  }
}

Using Hooks and cancel method

import React, { useState } from "react";
import {
  useAsyncEffect,
  E_REASON_UNMOUNTED,
  CanceledError
} from "use-async-effect2";
import cpAxios from "cp-axios";

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

  const cancel = useAsyncEffect(
    function* () {
      setText("fetching...");
      try {
        const response = yield cpAxios(
          `https://rickandmortyapi.com/api/character/${id}`
        ).timeout(props.timeout);
        setText(JSON.stringify(response.data, null, 2));
      } catch (err) {
        CanceledError.rethrow(err, E_REASON_UNMOUNTED);
        setText(err.toString());
      }
    },
    [id]
  );

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <div>{text}</div>
      <button
        className="btn btn-success"
        type="submit"
        onClick={() => setId(Math.round(Math.random() * 200))}
      >
        Fetch random character info
      </button>
      <button className="btn btn-warning" onClick={cancel}>
        Cancel request
      </button>
    </div>
  );
}
Zygotene answered 2/12, 2020 at 0:11 Comment(0)
F
0

one more alternative way is to wrap your async function in a wrapper that will handle the use case when the component unmounts

as we know function are also object in js so we can use them to update the closure values

const promesifiedFunction1 = (func) => {
  return function promesify(...agrs){
    let cancel = false;
    promesify.abort = ()=>{
      cancel = true;
    }
    return new Promise((resolve, reject)=>{
       function callback(error, value){
          if(cancel){
              reject({cancel:true})
          }
          error ? reject(error) : resolve(value);
       }
       agrs.push(callback);
       func.apply(this,agrs)
    })
  }
}

//here param func pass as callback should return a promise object
//example fetch browser API
//const fetchWithAbort = promesifiedFunction2(fetch)
//use it as fetchWithAbort('http://example.com/movies.json',{...options})
//later in componentWillUnmount fetchWithAbort.abort()
const promesifiedFunction2 = (func)=>{
  return async function promesify(...agrs){
    let cancel = false;
    promesify.abort = ()=>{
      cancel = true;
    }

    try {
      const fulfilledValue = await func.apply(this,agrs);
      if(cancel){
        throw 'component un mounted'
      }else{
        return fulfilledValue;
      }
    }
    catch (rejectedValue) {
      return rejectedValue
    }
  }
}

then inside componentWillUnmount() simply call promesifiedFunction.abort() this will update the cancel flag and run the reject function

Fant answered 27/4, 2021 at 15:0 Comment(0)
W
0

Just four steps:

1.create instance of AbortController::const controller = new AbortController()

2.get signal:: const signal = controller.signal

3.pass signal to fetch parameter

4.controller abort anytime:: controller.abort();

const controller = new AbortController()
const signal = controller.signal

function beginFetching() {
    var urlToFetch = "https://xyxabc.com/api/tt";

    fetch(urlToFetch, {
            method: 'get',
            signal: signal,
        })
        .then(function(response) {
            console.log('Fetch complete');
        }).catch(function(err) {
            console.error(` Err: ${err}`);
        });
}


function abortFetching() {
    controller.abort()
}
Whereto answered 24/7, 2021 at 16:10 Comment(0)
S
0

If you have a timeout clear them when component unmount.

useEffect(() => {
    getReusableFlows(dispatch, selectedProject);
    dispatch(fetchActionEvents());

    const timer = setInterval(() => {
      setRemaining(getRemainingTime());
    }, 1000);

    return () => {
      clearInterval(timer);
    };
  }, []);
Sarena answered 4/9, 2021 at 15:39 Comment(0)
P
0

There are many great answers here and i decided to throw some in too. Creating your own version of useEffect to remove repetition is fairly simple:

import { useEffect } from 'react';

function useSafeEffect(fn, deps = null) {
  useEffect(() => {
    const state = { safe: true };
    const cleanup = fn(state);
    return () => {
      state.safe = false;
      cleanup?.();
    };
  }, deps);
}

Use it as a normal useEffect with state.safe being available for you in the callback that you pass:

useSafeEffect(({ safe }) => {
  // some code
  apiCall(args).then(result => {
    if (!safe) return;
    // updating the state
  })
}, [dep1, dep2]);
Portray answered 3/11, 2021 at 14:34 Comment(0)
C
0

This is a more general solution for async/await and promises. I did this because my React callbacks were in between important async calls, so I couldn't cancel all the promises.

// TemporalFns.js
let storedFns = {};
const nothing = () => {};
export const temporalThen = (id, fn) => {
    if(!storedFns[id]) 
        storedFns[id] = {total:0}
    let pos = storedFns[id].total++;
    storedFns[id][pos] = fn;
    
    return data => { const res = storedFns[id][pos](data); delete storedFns[id][pos]; return res; }
}
export const cleanTemporals = (id) => {
    for(let i = 0; i<storedFns[id].total; i++) storedFns[id][i] = nothing;
}

Usage: (Obviously each instance should have different id)

const Test = ({id}) => {
    const [data,setData] = useState('');
    useEffect(() => {
        someAsyncFunction().then(temporalThen(id, data => setData(data))
            .then(otherImportantAsyncFunction).catch(...);
        return () => { cleanTemporals(id); }
    }, [])
    return (<p id={id}>{data}</p>);
}
Catgut answered 25/2, 2022 at 8:36 Comment(0)
K
0

we can create a custom hook to wrap the fetch function like this:

//my-custom-fetch-hook.js
import {useEffect, useRef} from 'react'
function useFetch(){
  const isMounted = useRef(true)

  useEffect(() => {
    isMounted.current = true   //must set this in useEffect or your will get a error when the debugger refresh the page
    return () => {isMounted.current = false}
  }, [])


  return (url, config) => {
    return fetch(url, config).then((res) => {
      if(!isMounted.current)
        throw('component unmounted')
      return res
    })
  }
}

export default useFetch

Then in our functional component:

import useFetch from './my-custom-fetch-hook.js'

function MyComponent(){
  const fetch = useFetch()
  ...  

  fetch(<url>, <config>) 
    .then(res => res.json())
    .then(json => { ...set your local state here})
    .catch(err => {...do something})
}
Kirima answered 18/4, 2022 at 2:23 Comment(0)
T
-4

I think I figured a way around it. The problem is not as much the fetching itself but the setState after the component is dismissed. So the solution was to set this.state.isMounted as false and then on componentWillMount change it to true, and in componentWillUnmount set to false again. Then just if(this.state.isMounted) the setState inside the fetch. Like so:

  constructor(props){
    super(props);
    this.state = {
      isMounted: false,
      isLoading: true,
      dataSource: [{
        name: 'loading...',
        id: 'loading',
      }]
    }
  }

  componentDidMount(){
    this.setState({
      isMounted: true,
    })

    return fetch('LINK HERE')
      .then((response) => response.json())
      .then((responseJson) => {
        if(this.state.isMounted){
          this.setState({
            isLoading: false,
            dataSource: responseJson,
          }, function(){
          });
        }
      })
      .catch((error) =>{
        console.error(error);
      });
  }

  componentWillUnmount() {
    this.setState({
      isMounted: false,
    })
  }
Tub answered 18/4, 2018 at 19:6 Comment(1)
setState is probably not ideal, since it won't update the value in state immediately.Heavily

© 2022 - 2024 — McMap. All rights reserved.