Thunks

Reducers are synchronous and pure, so a fetch cannot live in one. A thunk is the escape hatch: a function you pass to dispatch instead of an action object. The thunk middleware — installed by configureStore with no setup — calls it with (dispatch, getState, extraArgument), so the function can await, branch on the current state, and dispatch as many plain actions as it needs.

A thunk that reads state, awaits, and dispatches twiceJavaScript
const checkout = (state = { status: 'idle', order: null }, action) =>
  action.type === 'checkout/started' ? { ...state, status: 'sending' }
  : action.type === 'checkout/done' ? { status: 'paid', order: action.payload }
  : state;
const store = configureStore({ reducer: checkout });
const submitOrder = (total) => async (dispatch, getState) => {   // a thunk
  if (getState().status === 'sending') return 'ignored';         // no double submit
  dispatch({ type: 'checkout/started' });
  await new Promise(r => setTimeout(r, 200));       // stands in for POST /api/orders
  dispatch({ type: 'checkout/done', payload: { id: 'A-17', total } });
  return getState().order;                          // dispatch() returns the thunk's promise
};
const first = store.dispatch(submitOrder(27));
console.log('second dispatch ->', await store.dispatch(submitOrder(27)));
console.log('first dispatch  ->', await first);
Output
second dispatch -> ignored
first dispatch  -> { id: 'A-17', total: 27 }

A thunk's return value comes back from dispatch — here a promise, so the caller can await a save and then navigate. The duplicate submit is rejected by reading getState(), a check that belongs in the thunk because the component is not the only thing that can start a checkout. The third argument, configured once with getDefaultMiddleware({ thunk: { extraArgument: api } }), lets every thunk call api.post(...) without importing anything — that is how you swap in a fake client for tests. What thunks lack is structure: every one that loads data writes the same started/succeeded/failed trio by hand, which is what createAsyncThunk replaces.