224

is it possible to dispatch an action in a reducer itself? I have a progressbar and an audio element. The goal is to update the progressbar when the time gets updated in the audio element. But I don't know where to place the ontimeupdate eventhandler, or how to dispatch an action in the callback of ontimeupdate, to update the progressbar. Here is my code:

//reducer

const initialState = {
    audioElement: new AudioElement('test.mp3'),
    progress: 0.0
}

initialState.audioElement.audio.ontimeupdate = () => {
    console.log('progress', initialState.audioElement.currentTime/initialState.audioElement.duration);
    //how to dispatch 'SET_PROGRESS_VALUE' now?
};


const audio = (state=initialState, action) => {
    switch(action.type){
        case 'SET_PROGRESS_VALUE':
            return Object.assign({}, state, {progress: action.progress});
        default: return state;
    }

}

export default audio;
zoul
  • 96,282
  • 41
  • 242
  • 342
klanm
  • 2,708
  • 3
  • 17
  • 20

5 Answers5

188

Starting another dispatch before your reducer is finished is an anti-pattern, because the state you received at the beginning of your reducer will not be the current application state anymore when your reducer finishes. But scheduling another dispatch from within a reducer is NOT an anti-pattern. In fact, that is what the Elm language does, and as you know Redux is an attempt to bring the Elm architecture to JavaScript.

Here is a middleware that will add the property asyncDispatch to all of your actions. When your reducer has finished and returned the new application state, asyncDispatch will trigger store.dispatch with whatever action you give to it.

// This middleware will just add the property "async dispatch" to all actions
const asyncDispatchMiddleware = store => next => action => {
  let syncActivityFinished = false;
  let actionQueue = [];

  function flushQueue() {
    actionQueue.forEach(a => store.dispatch(a)); // flush queue
    actionQueue = [];
  }

  function asyncDispatch(asyncAction) {
    actionQueue = actionQueue.concat([asyncAction]);

    if (syncActivityFinished) {
      flushQueue();
    }
  }

  const actionWithAsyncDispatch =
    Object.assign({}, action, { asyncDispatch });

  const res = next(actionWithAsyncDispatch);

  syncActivityFinished = true;
  flushQueue();

  return res;
};

Now your reducer can do this:

function reducer(state, action) {
  switch (action.type) {
    case "fetch-start":
      fetch('wwww.example.com')
        .then(r => r.json())
        .then(r => action.asyncDispatch({ type: "fetch-response", value: r }))
      return state;

    case "fetch-response":
      return Object.assign({}, state, { whatever: action.value });;
  }
}
skivol
  • 3
  • 3
Marcelo Lazaroni
  • 7,543
  • 2
  • 30
  • 36
  • 9
    Marcelo, your blog post here does a great job describing the circumstances of your approach, so I'm linking to it here: https://lazamar.github.io/dispatching-from-inside-of-reducers/ – Dejay Clayton Oct 04 '17 at 13:57
  • 4
    This was exactly what I needed, except the middleware as-is breaks `dispatch` which should return the action. I changed the last lines to: `const res = next(actionWithAsyncDispatch); syncActivityFinished = true; flushQueue(); return res;` and it worked great. – zanerock Jun 07 '18 at 15:46
  • 1
    If you're not supposed to dispatch an action within a reducer, then is redux-thunk breaking the rules of redux? – Eric Wiener Aug 16 '18 at 02:16
  • How does this work when you try to handle websocket responses? This is my reducer export default (state, action) => { const m2 = [ ...state.messages, action.payload ] return Object.assign({}, state, { messages: m2, }) } and I STILL get this error "state mutation was detected in between dispatches" – quantumpotato Sep 26 '19 at 23:03
173

Dispatching an action within a reducer is an anti-pattern. Your reducer should be without side effects, simply digesting the action payload and returning a new state object. Adding listeners and dispatching actions within the reducer can lead to chained actions and other side effects.

Sounds like your initialized AudioElement class and the event listener belong within a component rather than in state. Within the event listener you can dispatch an action, which will update progress in state.

You can either initialize the AudioElement class object in a new React component or just convert that class to a React component.

class MyAudioPlayer extends React.Component {
  constructor(props) {
    super(props);

    this.player = new AudioElement('test.mp3');

    this.player.audio.ontimeupdate = this.updateProgress;
  }

  updateProgress () {
    // Dispatch action to reducer with updated progress.
    // You might want to actually send the current time and do the
    // calculation from within the reducer.
    this.props.updateProgressAction();
  }

  render () {
    // Render the audio player controls, progress bar, whatever else
    return <p>Progress: {this.props.progress}</p>;
  }
}

class MyContainer extends React.Component {
   render() {
     return <MyAudioPlayer updateProgress={this.props.updateProgress} />
   }
}

function mapStateToProps (state) { return {}; }

return connect(mapStateToProps, {
  updateProgressAction
})(MyContainer);

Note that the updateProgressAction is automatically wrapped with dispatch so you don't need to call dispatch directly.

ofundefined
  • 1,131
  • 1
  • 10
  • 25
ebuat3989
  • 4,701
  • 3
  • 21
  • 17
  • thank you very much for the clarification! But I still don't know how to access the dispatcher. I always used the connect method from react-redux. but I don't know how to call it in the updateProgress method. Or is there another way to get the dispatcher. maybe with props? thank you – klanm Apr 19 '16 at 23:42
  • No problem. You can pass in the action to the ``MyAudioPlayer`` component from the parent container that is ``connect``ed with ``react-redux``. Check out how to do that with ``mapDispatchToProps`` here: https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options – ebuat3989 Apr 19 '16 at 23:47
  • ahhh nice, now I get the concepts! It works! thank you very much! It is still all mystic magic for me :) – klanm Apr 19 '16 at 23:54
  • 6
    Where is the symbol `updateProgressAction` defined in your example? – Charles Prakash Dasari Jan 27 '18 at 00:48
  • 2
    If you're not supposed to dispatch an action within a reducer, then is redux-thunk breaking the rules of redux? – Eric Wiener Aug 16 '18 at 02:16
  • What happens if you need to know what your current state is to know when to dispatch? For example fetching normalised data. Sometimes you are fetching and you get a response with references. These references point to other data that you need to further fetch. However you don't just want to dispatch within the same async action, since you also need to know your current state to see if you already have that state partially. – CMCDragonkai Jun 26 '19 at 08:50
  • 2
    @EricWiener I believe `redux-thunk` is dispatching an action from another action, not the reducer. https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559 – sallf Sep 17 '19 at 20:12
17

You might try using a library like redux-saga. It allows for a very clean way to sequence async functions, fire off actions, use delays and more. It is very powerful!

chandlervdw
  • 3,017
  • 3
  • 17
  • 20
  • 2
    can you specify how to achieve 'scheduling another dispatch inside reducer' with redux-saga? – XPD Dec 16 '17 at 12:47
  • 1
    @XPD can you explain a little more what you're wanting to accomplish? If you're trying to use a reducer action to dispatch another action, you won't be able to without something akin to redux-saga. – chandlervdw Dec 18 '17 at 14:26
  • 1
    For example, suppose you have a item store where you have loaded a part of the items. Items are loaded lazily. Assume an item has a supplier. Suppliers also loaded lazily. So in this case there might be an item which its supplier hasn't been loaded. In an item reducer, if we need to get information about an item which hasn't been loaded yet, we have to load data from server again through an reducer. In that case how does redux-saga help inside a reducer? – XPD Dec 19 '17 at 11:55
  • 1
    Ok, let's say you wanted to fire off this request for `supplier` info when the user attempts to visit the `item` details page. Your `componentDidMount()` would fire off a function that dispatches an action, say `FETCH_SUPPLIER`. Within the reducer, you may tick on something like a `loading: true` to show a spinner while the request is made. `redux-saga` would listen for that action, and in response, fire off the actual request. Utilizing generator functions, it can then wait for the response and dump it into `FETCH_SUPPLIER_SUCCEEDED`. The reducer then updates the store with supplier info. – chandlervdw Dec 20 '17 at 14:57
6

redux-loop takes a cue from Elm and provides this pattern.

Quang Van
  • 9,955
  • 11
  • 49
  • 54
0

Dispatching and action inside of reducer seems occurs bug.

I made a simple counter example using useReducer which "INCREASE" is dispatched then "SUB" also does.

In the example I expected "INCREASE" is dispatched then also "SUB" does and, set cnt to -1 and then continue "INCREASE" action to set cnt to 0, but it was -1 ("INCREASE" was ignored)

See this: https://codesandbox.io/s/simple-react-context-example-forked-p7po7?file=/src/index.js:144-154

let listener = () => {
  console.log("test");
};
const middleware = (action) => {
  console.log(action);
  if (action.type === "INCREASE") {
    listener();
  }
};

const counterReducer = (state, action) => {
  middleware(action);
  switch (action.type) {
    case "INCREASE":
      return {
        ...state,
        cnt: state.cnt + action.payload
      };
    case "SUB":
      return {
        ...state,
        cnt: state.cnt - action.payload
      };
    default:
      return state;
  }
};

const Test = () => {
  const { cnt, increase, substract } = useContext(CounterContext);

  useEffect(() => {
    listener = substract;
  });

  return (
    <button
      onClick={() => {
        increase();
      }}
    >
      {cnt}
    </button>
  );
};

{type: "INCREASE", payload: 1}
{type: "SUB", payload: 1}
// expected: cnt: 0
// cnt = -1
JillAndMe
  • 1,831
  • 2
  • 14
  • 36