3

Within my React component, I have an async request which dispatches an action to my Redux store which is called within the useEffect hook:

    const loadFields = async () => {
        setIsLoading(true);
        try {
            await dispatch(fieldsActions.fetchFields(user.client.id));
        } catch (error) {
            setHasError(true);
        }
        setIsLoading(false);
    }
    useEffect(() => { if(isOnline) { loadFields() } }, [dispatch, isOnline]);

The action requests data via a fetch request:

    export const fetchFields = clientId => {
        return async dispatch => {
            try {
                const response = await fetch(
                    Api.baseUrl + clientId + '/fields',
                    { headers: { 'Apiauthorization': Api.token } }
                );

                if (!response.ok) {
                    throw new Error('Something went wrong!');
                }

                const resData = await response.json();
                dispatch({ type: SET_FIELDS, payload: resData.data });
            } catch (error) {
                throw error;
            }
        }
    };

    export const setFields = fields => ({
        type    : SET_FIELDS,
        payload : fields
    });

When this is rendered within the React app it results in the following 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 %s.%s, a useEffect cleanup function

I believe this occurs because the promise doesn't have a "clean-up" function. But I am unsure where to place this? Should I have some logic within LoadFields()? Or must this be done within the useEffect hook?

Sheixt
  • 2,546
  • 12
  • 36
  • 65
  • Is your question duplicated? https://stackoverflow.com/questions/38329209/how-to-cancel-abort-ajax-request-in-axios – chin8628 Sep 30 '19 at 10:35
  • Not really as this one is using react-hooks + does not have an issue with the http call itself – oktapodia Sep 30 '19 at 10:36
  • @chin8628 I agree with @oktapodia as that question also relates specifically to axios. My issue is with understanding removing the subscription within `useEffect` as part of an `async` function. – Sheixt Sep 30 '19 at 10:56

2 Answers2

4

This tutorial which will help you to resolve your issue.

Quick example: with Promises

function BananaComponent() {

  const [bananas, setBananas] = React.useState([])

  React.useEffect(() => {
    let isSubscribed = true
    fetchBananas().then( bananas => {
      if (isSubscribed) {
        setBananas(bananas)
      }
    })
    return () => isSubscribed = false
  }, []);

  return (
    <ul>
    {bananas.map(banana => <li>{banana}</li>)}
    </ul>
  )
}

Quick example: with async/await (Not the best one but that should work with an anonymous function)

function BananaComponent() {

  const [bananas, setBananas] = React.useState([])

  React.useEffect(() => {
    let isSubscribed = true
    async () => {
      const bananas = await fetchBananas();
      if (isSubscribed) {
        setBananas(bananas)
      }
    })();

    return () => isSubscribed = false
  }, []);

  return (
    <ul>
    {bananas.map(banana => <li>{banana}</li>)}
    </ul>
  )
}
Eric Leschinski
  • 146,994
  • 96
  • 417
  • 335
oktapodia
  • 1,695
  • 1
  • 15
  • 31
  • thank you for the resource. In that example they use `.then()` to apply the action upon completion of the `fetch()` call. As I am using the `await` keyword for `useDispatch` within `useEffect` where would the logic to cancel the subscription exist? – Sheixt Sep 30 '19 at 11:04
2

First issue
If your useEffect() fetches data acynchronously then it would be a very good idea to have a cleanup function to cancel the non-completed fetch. Otherwise what could happen is like that: fetch takes longer than expected, meantime the component is re-rendered for whatever reason. Maybe because its parent is re-rendered. The cleanup of useEffect runs before re-render and the useEffect itself runs after re-render. To avoid having another fetch inflight it's better to cancel the previous one. Sample code:

const [data, setData] = useState();
useEffect(() => {
  const controller = new AbortController();
  const fetchData = async () => {
    try {
      const apiData = await fetch("https://<yourdomain>/<api-path>",
                               { signal: controller.signal });
      setData(apiData);
    } catch (err) {
      if (err.name === 'AbortError') {
        console.log("Request aborted");
        return;
      }
    }
  };

  fetchData();
  return () => {
    controller.abort();
  }
});

Second issue
This code

return async dispatch => {

will not work because neither dispatch nor Redux store support async actions. The most flexible and powerful way to handle this issue is to use middleware like redux-saga. The middleware lets you:

  • dispatch 'usual' sync actions to Redux store.
  • intercept those sync actions and in response make one or several async calls doing whatever you want.
  • wait until async call(s) finish and in response dispatch one or several sync actions to Redux store, either the original ones which you intercepted or different ones.
winwiz1
  • 2,906
  • 11
  • 24