8

I found a lot of examples for this, but none uses Redux and useDispatch, so here's the situation ...

I have an action file with an async action like this ...

//postsActions.tsx

export const asyncGetPostById = (postId: string) => {
    return async (dispatch: any, getState: () => AppState) => {
        try {
            dispatch(startLoader());
            const { data } = await axios.get(`api/posts/${postId}`);
            dispatch(setPostDataInReducer(data));
        } catch (error) {
            console.log(error)
        }
    };
};

And I'm calling this action inside the Post.tsx component like this ...

const dispatch = useDispatch();

useEffect(() => {
    dispatch(asyncGetPostById(postId));
}, []);

This is of course a simplified version, but it's the same idea. Now how can I clean up a dispatch like that in useEffect to avoid this error ...

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.

Edit

The idea is how to cancel the request on ComponentWillUnmount (or the cleanup function in useEffect in my case) if it's not done already so that you won't have stale or unresolved requests in the background that will make your app slow.

It's a very common issue that a lot of people fall a victim to, and there's a lot of solutions out there, but not for this particular case (with dispatch and redux).

Some uses AbortController(), and others use axios.CancelToken but I'm not sure how to use either of them in my case.

Ruby
  • 2,207
  • 12
  • 42
  • 71
  • Is the problem that the component might have been unmounted by the time the `axios.get` resolves? I can think of an ugly solution (pass in an object which gets mutated on unmount), hope there's something better – CertainPerformance Sep 17 '20 at 19:15
  • Yes that's the problem, some uses `axios.CancelToken`, and others use `AbortController`, but I'm not sure how to use these here in this situation with dispatch and redux. – Ruby Sep 17 '20 at 19:32
  • The idea is how to cancel the request on ComponentWillUnmount if it's not done already so that you won't have stale or unresolved requests in the background that will make your app slow. – Ruby Sep 17 '20 at 19:34
  • This shouldn't throw an error unless you're setting something to state after the request has been fulfilled. However, if you want to cancel a request, then you'll use one of options you've mentioned (although [AbortController](https://caniuse.com/abortcontroller) won't work in some browsers). Another approach would be to use redux sagas instead of thunks, which allow you to manually cancel tasks. That said, without a repo or a working demo, it's hard to recommend a best approach recommendation. – Matt Carlotta Sep 17 '20 at 19:48
  • @MattCarlotta I do change the state or rather the reducer after the request, and in some cases, I dispatch another async action that also updates the reducer. I just need to know how to cancel a request in the mentioned example, that's it. – Ruby Sep 17 '20 at 19:55
  • If you're just updating Redux state, then there's most likely a Redux implementation issue. The Provider (state manager) should always be and stay mounted across page navigation, so it shouldn't be throwing any warnings. Again, hard to determine the problem without a [mcve](https://stackoverflow.com/help/minimal-reproducible-example). – Matt Carlotta Sep 17 '20 at 20:04
  • @Ruby I've put together a [NextJS with Redux Thunk example](https://github.com/mattcarlotta/with-redux-thunk-example) that you can run locally. As you'll notice, no warnings are thrown during page navigation. HOWEVER, you will notice that if you navigate quickly between `/success` and `/failure` that you'll have a racing condition. As such, you'll need to handle it by using one of the options mentioned above. In most use cases, this racing condition can be avoided if you containerize the action to a single parent component. – Matt Carlotta Sep 17 '20 at 21:02

2 Answers2

1

Try this for your action creator:

export const asyncGetPostById = (postId: string) => async (dispatch: Function, getState: () => AppState) => {
    try {
        dispatch(startLoader());
        const { data } = await axios.get(`api/posts/${postId}`);
        dispatch(setPostDataInReducer(data));
    } catch (error) {
        console.log(error)
    }
};

It's a subtle change, but with this action creator you're not actually returning anything (In TypeScript it would be void)

With a void return, your dispatch calls in the action creator will go straight to your reducer, but in your component nothing will be returned, so you shouldn't have to cleanup anything.

Edit:

Additionally, you should consider adding dispatch to your useEffects dependency array:

const dispatch = useDispatch();

useEffect(() => {
    dispatch(asyncGetPostById(postId));
}, [dispatch]);
rantao
  • 1,621
  • 4
  • 14
  • 34
  • Well, I found out that the issue I was having is because I was closing the popup before the dispatch was complete, so all I had to do is to make async-await function inside the popup and await the dispatch then close the popup, and it worked. But still, I'm still wondering how to clean up a dispatch inside useEffect. – Ruby Sep 18 '20 at 11:21
  • Glad you resolved the issue. As far as I know you don't need to cleanup a redux dispatch; you're not making any subscriptions with it so you shouldn't have to cleanup anything. – rantao Sep 18 '20 at 15:57
-1

You must to create in your reducer a case that clean the current state. And then in useEffect you do like that

React.useEffect(()=> {
 dispatch(your_action)
 return () => dispatch(clean_data_action)
},[])
 
Florin
  • 292
  • 2
  • 13