Search code examples
reactjsreduxredux-thunk

About Redux-thunk, if a handler dispatches a function to be called, why not call it directly?


As I understand redux-thunk, it can be

<button onClick={fn1}>Click Me</button>

and in fn1, it dispatches not an action object, but dispatches a function fn2, so that when the redux-thunk middleware calls it ( meaning fn2), invoke a fetch or Ajax call, and let the then fulfillment handler or the callback of Ajax dispatch an action object, like

{ type: "DATA_RECEIVED", data: data }

but why doesn't fn1 directly call fn2 to do the task, but let the middleware call it instead?


Solution

  • Redux Thunk is a very thin middleware (like 14 lines thin) that introduces the convention that Redux should know how to deal with asynchronous processes for you.

    You said

    the thing is you do want fn2 to fire immediately. Such as: user clicks a button to fetch something, you do want the function that does the fetch or Ajax to fire immediately. I think with redux-thunk, if you dispatch a function, it is fired immediately

    You're right, you do want it to fire immediately, but do you want to be the one that manually wires up calling the function and passing the dispatch or do you want Redux Thunk to do it for you?

    No matter what, Redux is going to need to dispatch an action at some later time. Redux Thunk does the "later" for you.

    Consider these differences:

    const store = {
      dispatch() {
        console.log("Dispatch");
      }
    };
    
    // Action creator that retuns an action
    const inc = () => ({
      type: "INCREMENT"
    });
    
    // Impure action creator that you shouldn't use
    const badAsyncInc = () => {
      setTimeout(() => {
        store.dispatch({
          type: "INCREMENT"
        });
      }, 1000);
    };
    
    // Action creator that returns a function that takes dispatch as arg
    const aInc = () => dis => setTimeout(() => dis({
      type: "INCREMENT"
    }), 1000);
    
    /*
     * The following will do everything including waiting to dispatch
     * and dispatching to the store.  That's good, but not good for
     * when we want to use a different store.
     */
    badAsyncInc()();
    
    /*
     * The following is the "manual" way of wiring up the delayed
     * dispatch that you have to do without Redux Thunk.
     */
    aInc()(store.dispatch);
    
    /*
     * With Redux Thunk, this will call the "fn2" immediately but also
     * pass the dispatch function to "fn2" for you so that you don't 
     * have to wire that up yourself.
     */
    store.dispatch(aInc);
    

    In this instance, you could just call badAsyncInc and be done since it will call store.dispatch for you. But what if you have multiple stores? What if you want to use a testing harness' store? What if you want to mock the store completely? Your badAsyncInc is now coupled with the store which is bad for action creators.

    Alternatively, you can just call aInc yourself and pass it a dispatch function. However, why do that when all your other action creators are probably being written as store.dispatch(actionCreator()). Redux Thunk helps you keep that pattern, even with async actions.

    Redux Thunk tries to help you write action creators that are loosely coupled and testable.