Nidhin Kumar
Nidhin Kumar

Reputation: 3579

How to stop memory leak in useEffect hook react

I am using Effect hook to fetch the datas from server and these data are passed to the react table there i have used the same api call to load the next set of datas from server. When the application gets loaded i am getting an warning like below

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.

Effect Hook:

useEffect(() => {
setPageLoading(true);
props
  .dispatch(fetchCourses())
  .then(() => {
    setPageLoading(false);
  })
  .catch((error: string) => {
    toast.error(error);
    setPageLoading(false);
  });
}, []);

React Table Page:

<ReactTable
  className="-striped -highlight"
  columns={columns}
  data={coursesData}
  defaultPage={currentPage}
  defaultPageSize={courses.perPage}
  loading={isLoading}
  manual={true}
  onFetchData={setFilter}
/>

Set Filter function:

const setFilter = (pagination: any) => {
  props.dispatch(updateCoursePageSize(pagination.pageSize));
  props.dispatch(updateCourseCurrentPage(pagination.page + 1));
  setCurrentPage(pagination.page);
  setPerPage(pagination.pageSize);
  setLoading(true);
  props.dispatch(fetchCourses()).then(() => {
    setLoading(false);
  });
};

Does anyone know how to clean up the hook in react

Upvotes: 33

Views: 97670

Answers (4)

Nicholas Tower
Nicholas Tower

Reputation: 85022

June 2022 update (still accurate at end of 2023):

React 18 has removed this warning message, and the workarounds to get rid of it may no longer be necessary. Part of the reason they removed it is that it has always been a bit misleading. It says you have a memory leak, but often times you don't.

The code in the question -- and indeed most code that causes this warning -- runs for a finite amount of time past the unmounting of the component, then sets state, then is done running. Since it's done running, javascript can free up variables in its closure, and thus there is usually no leak.

The case where you will have a memory leak is if you are setting up a persistent subscription which continues indefinitely. For example, maybe you set up a websocket and listen to messages, but you never tear down that websocket. These cases do need to be fixed (by supplying a cleanup function to the useEffect) but they are uncommon.

The other reason react 18 has removed the warning is that they are working on the ability for components to preserve their state after being unmounted. Once that feature is in react, setting state after unmount will be a perfectly valid thing to do.

Original answer (September 2019):

With useEffect you can return a function that will be run on cleanup. So in your case, you'll want something like this:

useEffect(() => {
  let unmounted = false;

  setPageLoading(true);

  props
    .dispatch(fetchCourses())
    .then(() => {
      if (!unmounted) {
        setPageLoading(false);
      }
    })
    .catch((error: string) => {
      if (!unmounted) {
        toast.error(error);
        setPageLoading(false);
      }
    });

  return () => { unmounted = true };
}, []);

EDIT: if you need to have a call that's kicked off outside of useEffect, then it will still need to check an unmounted variable to tell whether it should skip the call to setState. That unmounted variable will be set by a useEffect, but now you need to go through some hurdles to make the variable accessible outside of the effect.

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

  const setFilter = () => {
    // ...
    props.dispatch(fetchCourses()).then(() => {
      if (!unmounted.current) {
        setLoading(false);
      }
    })
  }

  // ...
  return (
    <ReactTable onFetchData={setFilter} /* other props omitted */ />
  );
}

Upvotes: 66

Ilan Roitlender
Ilan Roitlender

Reputation: 21

you can create a custom hook for that like that :

import * as React from 'react';

export default function useStateWhenMounted<T>(initialValue: T) {
  const [state, setState] = React.useState(initialValue);
  const isMounted = React.useRef(true);
  React.useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  const setNewState = React.useCallback((value) => {
    if (isMounted.current) {
      setState(value);
    }
  }, []);

  return [state, setNewState];
}

Upvotes: 2

Mohan Teja Chitturi
Mohan Teja Chitturi

Reputation: 169

Memory leak happens, when a thing that is unnecessary and is supposed to be cleared from memory is kept because some other thing is still holding it. In React Component case, the async call made in component may hold the references of setState or other references and will hold them until the call completes. The warning you see is from React saying that something is still holding and setting state of a component instance that was removed from tree long back when component unmounted. Now using a flag to not set the state only removes the warning but not the memory leak, even using Abort controller does the same. To escape this situation you can use state management tools that helps dispatching an action which will do processing out side of component without holding any memory references of the component, for example redux. If you are not using such tools then you should find a way to clear the callbacks you pass to the async call (then, catch, finally blocks) when component unmounts. In the below snippet I am doing the same detaching the references to the methods passed to async call to avoid memory leaks. Event Emitter here is an Observer, you can create one or use some package.

const PromiseObserver = new EventEmitter();

class AsyncAbort {
  constructor() {
    this.id = `async_${getRandomString(10)}`;
    this.asyncFun = null;
    this.asyncFunParams = [];
    this.thenBlock = null;
    this.catchBlock = null;
    this.finallyBlock = null;
  }

  addCall(asyncFun, params) {
    this.asyncFun = asyncFun;
    this.asyncFunParams = params;
    return this;
  }

  addThen(callback) {
    this.thenBlock = callback;
    return this;
  }

  addCatch(callback) {
    this.catchBlock = callback;
    return this;
  }

  addFinally(callback) {
    this.finallyBlock = callback;
    return this;
  }

  call() {
    const callback = ({ type, value }) => {
      switch (type) {
        case "then":
          if (this.thenBlock) this.thenBlock(value);
          break;
        case "catch":
          if (this.catchBlock) this.catchBlock(value);
          break;
        case "finally":
          if (this.finallyBlock) this.finallyBlock(value);
          break;
        default:
      }
    };
    PromiseObserver.addListener(this.id, callback);
    const cancel = () => {
      PromiseObserver.removeAllListeners(this.id);
    };
    this.asyncFun(...this.asyncFunParams)
      .then((resp) => {
        PromiseObserver.emit(this.id, { type: "then", value: resp });
      })
      .catch((error) => {
        PromiseObserver.emit(this.id, { type: "catch", value: error });
      })
      .finally(() => {
        PromiseObserver.emit(this.id, { type: "finally" });
        PromiseObserver.removeAllListeners(this.id);
      });
    return cancel;
  }
}

in the useEffect hook you can do

React.useEffect(() => {
    const abort = new AsyncAbort()
      .addCall(simulateSlowNetworkRequest, [])
      .addThen((resp) => {
        setText("done!");
      })
      .addCatch((error) => {
        console.log(error);
      })
      .call();
    return () => {
      abort();
    };
  }, [setText]);

I forked someones code from here to use above logic, you can check it in action in the below link link

Upvotes: 1

Nans .D
Nans .D

Reputation: 113

The other answers work of course, I just wanted to share a solution I came up with. I built this hook that works just like React's useState, but will only setState if the component is mounted. I find it more elegant because you don't have to mess arround with an isMounted variable in your component !

Installation :

npm install use-state-if-mounted

Usage :

const [count, setCount] = useStateIfMounted(0);

You can find more advanced documentation on the npm page of the hook.

Upvotes: -1

Related Questions