Author avatar

Chris Parker

Different Ways to Dispatch Actions with Redux

Chris Parker

  • Oct 24, 2019
  • 12 Min read
  • 46 Views
  • Oct 24, 2019
  • 12 Min read
  • 46 Views

Introduction

In Javascript, Redux is used as state container for many apps, mostly with React. It relies on actions that get dispatched and listened by reducers, which update the state accordingly.

In this guide, we'll look at different ways of dispatching the action and at isolating the components from Redux.

Let's consider an example in which a service allows a user to send any message to a group. We'll create a form MessageSender.js and hit the send button to send the message to an API to handle.

Passing dispatch method to our component

The dispatch method is available on the store object. An action gets dispatched to trigger an update to the Redux store.

1
2
3
4
5
6
7
8
9
10
11
// App.js
import { createStore } from 'redux';
import { MessageSender } from './MessageSender';
import reducer from './reducer';

const store = createStore(reducer);
class App extends React.Component {
 render() {
 <MessageSender store={store} />
 };
};
javascript
1
2
3
4
5
// MessageSender.js
import { sendMsg } from './actions';
// ...
this.props.store.dispatch(sendMsg(msg))
// ...
javascript

Using React-Redux to make dumb/smart components

The downside of the above approach is that our React component is aware of the app logic. It’s best to separate the logic in our smart component connected to the store from the user interface, i.e., from the dumb component. We can create a MessageSender.container.js in which we connect the component using the connect method. The second argument to connect is the function mapDispatchToProps, which is a wrapper for all the action creators using the dispatch method and is passed to the component.

From the official docs for connect, we can describe mapDispatchToProps this way: If an object is passed, each function inside it is assumed to be a Redux action creator. An object with the same function names, but with every action creator wrapped into a dispatch call so they may be invoked directly, will be merged into the component’s props.

The above implies that we do not need to dispatch our actions. We just need to pass an object to "connect”. We can then call the wrapped actions from our props.

Below is the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// MessageSender.container.js
import { connect } from 'react-redux';
import { sendMsg } from './actions';
import MessageSender from './MessageSender';

const mapDispatchToProps = {
 sendMsg
};

export default connect(null, mapDispatchToProps)(MessageSender);

// MessageSender.js
// ...
this.props.sendMsg(msg);
// ...
javascript

If we want to dispatch many actions in a single method, we can do that as shown below:

1
2
3
4
5
6
7
8
9
10
11
import { connect } from 'react-redux';
import { sendMsg, navigateTo } from './actions';

const mapDispatchToProps = dispatch => ({
 sendMsg: msg => {
 	dispatch(sendMsg(msg));
 	dispatch(navigateTo({ routeName: 'myMsgList' }));
 }
});

export default connect(null, mapDispatchToProps)(MessageSender);
javascript
1
2
3
4
5
// MessageSender.js
// ...
this.props.sendMsg(msg);
// ...
});
javascript

Dispatch actions using Redux-saga library

To run many async instructions one after the other and also maintain readability of the code, we can dispatch an action that will then trigger a saga. We can use redux-saga library in the same way. Using the saga, we can dispatch actions with the put effect.

To install the library, we use the following code:

1
npm install redux-saga
javascript

Below is the JS for our component:

1
2
3
4
5
6
7
8
9
10
// my-sagas.js
import { put } from 'redux-saga/effects';
import { sendMsg, setLoading, navigateTo } from './actions';

export function* sendMsg(action) {
 yield put(setLoading('sendMsgPage', true));
 yield put(sendMsg(action.payload.message));
 yield put(navigateTo({routeName: 'myMsgList'}));
 yield put(setLoading('sendMsgPage', false));
}
javascript

Another example of making async calls using saga is shown below.

We'll create a basic Saga:

1
2
3
export function* heySaga() {
  console.log('Hi there from a basic Sagas example!')
}
javascript

Below is our main.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

// ...
import { heySaga } from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(heySaga)

const action = type => store.dispatch({type})

// rest
javascript

And below is our counterexample showing async calls:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const MyCounter = ({ value, onInc, onDec, onIncAsync }) =>
  <div>
    <button onClick={onIncAsync}>
      Increase after 1 second
    </button>
    {' '}
    <button onClick={onInc}>
      Increase
    </button>
    {' '}
    <button onClick={onDec}>
      Decrease
    </button>
    <hr />
    <div>
      You clicked on the button {value} times
    </div>
  </div>
javascript

We'll then connect the onIncAsync of our React Component to Store action. We'll update main.js module as shown below:

1
2
3
4
5
6
7
8
9
10
function render() {
  ReactDOM.render(
    <MyCounter
      value={store.getState()}
      onInc={() => action('INC')}
      onDec={() => action('DEC')}
      onIncAsync={() => action('INC_ASYNC')} />,
    document.getElementById('root')
  )
}
javascript

Unlike in redux-thunk, our component here dispatches a plain object action. We'll add the following code in sagas.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { put, takeEvery } from 'redux-saga/effects'
const delay = (ms) => new Promise(response => setTimeout(response, ms))

// ...

// Our Saga worker would perform the async inc task
export function* incAsync() {
  yield delay(2000)
  yield put({ type: 'INC' })
}

// Our Saga watcher would spawn a new incAsync task on each INC_ASYNC
export function* watchIncAsync() {
  yield takeEvery('INC_ASYNC', incAsync)
}
javascript

Sagas are generally implemented using generator functions, which yield objects to our redux-saga middleware. The yielded objects are like instructions which are meant to be interpreted by our middleware. When we yield a promise to our middleware, the middleware will suspend the saga until the time the pPromise is completed. As per the above code, the incAsync saga is suspended until the promise that is returned by delay is resolved, which would happen after 2 seconds.

Once we resolve the promise, our middleware will resume the Saga and run code until the next yield. In our example, the next statement is other yielded object, i.e., the result of calling put({type: 'INC'}), thus instructing our middleware to dispatch an INC action.

In the example above, "put" can be called as an "effect." Effects are plain JS objects which have instructions to be fulfilled by our middleware. When our middleware retrieves an effect yielded by a saga, the saga is paused until the effect is completed.

Thus, in short, the incAsync Saga sleeps for 2 seconds through the call to delay(2000). It then dispatches an INC action.

We have also created another saga, watchIncAsync. We use "takeEvery" helper function provided by redux-saga library to listen for dispatched INC_ASYNC actions and run "incAsync" each time.

We have 2 sagas now, and we have to start them both immediately. To do that, we'll add a mainSaga which would be responsible for starting our other sagas. We'll update the code in sagas.js as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { put, takeEvery, all } from 'redux-saga/effects'

const delay = (ms) => new Promise(response => setTimeout(response, ms))

function* heySaga() {
  console.log('Hi there from a basic Sagas example!');
}

function* incAsync() {
  yield delay(2000)
  yield put({ type: 'INC' })
}

function* watchIncAsync() {
  yield takeEvery('INC_ASYNC', incAsync)
}

// you can see that we just export the mainSaga
// This is the single entry point to start all our sagas immediately
export default function* mainSaga() {
  yield all([
    heySaga(),
    watchIncAsync()
  ])
}
javascript

This saga would yield an array containing the results of calling the two sagas; heySaga and watchIncAsync. Thus the two resulting generators would get started in parallel. Now we only need to call sagaMiddleware.run on the main saga in main.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
// ...
import mainSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)

const action = type => store.dispatch({type})

// rest
javascript

Passing callback function to our child component

It is considered good practice to call all actions from containers only. To do that, we can pass our callback function as props to all our child components. Thus, each child component would contain a list of all the callback functions defined in their prop types. The parent component can just pass function reference to each of our child component.

Below is an example. Let's say we have a container class called MyReactContainer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@connect(store => ({ myReactStore: store.myReactStore }))
class MyReactContainer extends Component {
  onSomeTaskStart = (payload) => {
    this.props.dispatch(MyActionTypes.SOME_TASK_START, payload);
  }
  onSomeTaskEnd = (payload) => {
    this.props.dispatch(MyActionTypes.SOME_TASK_END, payload);
  }
  render() {
    return (
      <MyComponent
        onSomeTaskStart={this.onSomeTaskStart}
        onSomeTaskEnd={this.onSomeTaskEnd}
      />
  }
}
javascript

Below is our component code:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyReactComponent extends Component {
  static propTypes = {
    onSomeTaskStart: PropsTypes.func,
    onSomeTaskEnd: PropTypes.func,
  }
  render() {
    return (
      <div>
        // we'll have the view code here
      </div>
    )
  }
}
javascript

Thus, we can see that the child can specify all the functions it exposes to the parent using its prop-types. Also, it does not have to worry about which actions to call. The parent component can either implement certain behaviors or can even ignore them. It is up to the child component to force implementation of some functionalities by defining prop-type as required.

Using the bindActionCreators method

The bindActionCreators method allows us to dispatch actions from any React component that is not connected to the store as "mapDispatchToPros" in the connect function of react-redux. It's not very common, as react-redux would work in most cases. However, let's have a look at an example. Say we have a MsgSenderPage that is connected and has the dispatch method inside its props. The MsgSenderPage contains a MsgSending component which we would like to be completely unaware of redux (i.e it does not have the dispatch method).

Below is the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// MsgSenderPage.js
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from './actions';

class MsgSenderPage extends React.Component {
 constructor(props) {
 super(props);
 const { dispatch } = props;
 this.boundedActions = bindActionCreators(actions, dispatch);
 }

 render() {
 return <MsgSending {...this.boundedActions} />;
 }
}

export default connect()(MsgSenderPage);
javascript

We can also use bindActionCreators and extend our props as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
import { bindActionCreators } from 'redux';
// ...
const mapDispatchToProps = dispatch => ({
  ...bindActionCreators(
    {
      sendMsg,
      deleteMsg,
    },
    dispatch,
  ),
  someOtherService, // this does not have to be wrapped in dispatch
})
javascript

Conclusion

We’ve looked at the various ways to dispatch actions with Redux. No single option is considered the best, as it fully depends on our use case. When we want to dispatch an action from our React component, we need to first connect it with the store and use the "connect" method of react-redux, i.e., the second approach discussed above. When we start having logic in our mapDispatchToProps function, we can dispatch action in our saga, i.e., the third approach, which we discussed in detail.

2