Home > Net >  How to Deal with JavaScript Closure when Using Async Code from a Separate Function
How to Deal with JavaScript Closure when Using Async Code from a Separate Function

Time:06-06

I'm trying to create a cleanup function for my useEffect and update the variable cancelRequest that is being used by the code from my getAllTweets() function.

let cancelRequest = false;

useEffect(() => {
    if (!url) {
        dispatch({ type: "IDLE" });
        return;
    }

    getAllTweets();

    return function cleanup() {
        cancelRequest = true;
    };
}, [state.page]);

...

function getAllTweets() {
    axiosConfig
        .get(`${url}?page=${state.page}`)
        .then((response) => {
            if (cancelRequest) {
                dispatch({ type: "IDLE" });
                return;
            } else if (state.page !== -1) {
                state.page === 1
                    ? dispatch({ type: "LOADED", payload: response.data.data })
                    : dispatch({
                            type: "LOADED",
                            payload: [...state.tweets, ...response.data.data],
                      });
            }

            ...
        })

        ...
}

But I think the variable wasn't being updated as I'm still getting the Can't perform a React state update on an unmounted component warning on my app when I'm trying to exit while it was fetching data.

CodePudding user response:

useReducer performs the same sort of role as useState - it stores state but is said to be useful for handling complex state logic. But still, if you use useReducer in a component, and that component becomes unmounted, you shouldn't call dispatch again, because it corresponds to state in that component only. Even just doing

dispatch({ type: "IDLE" });

will produce a React warning.

But there's another potential issue. cancelRequest is a component-scoped variable, which means there will be a separate binding for that variable each render - so reassigning cancelRequest in only a single render may not be enough in case there's also an async action going on from a prior render. Use a ref instead, which is persistent across all renders. The cleanup to change the ref should be in a separate useEffect(.., []) so that it'll run only when the component unmounts, rather than every time the page changes.

const cancelReqestRef = useRef();
useEffect(() => {
    // When unmounting, and only when unmounting, change the ref
    return () => cancelReqestRef.current = true;
}, []);
useEffect(() => {
    if (!url) {
        dispatch({ type: "IDLE" });
        return;
    }
    getAllTweets();
}, [state.page]);

...

function getAllTweets() {
    axiosConfig
        .get(`${url}?page=${state.page}`)
        .then((response) => {
            if (cancelReqestRef.current) {
                // DO NOT update state here with dispatch, just return
                return;
            } else if (state.page !== -1) {
                state.page === 1
                    ? dispatch({ type: "LOADED", payload: response.data.data })
                    : dispatch({
                            type: "LOADED",
                            payload: [...state.tweets, ...response.data.data],
                      });
            }

            ...
        })

        ...
}
  • Related