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.
The dispatch method is available on the store object. An action gets dispatched to trigger an update to the Redux store.
1// App.js
2import { createStore } from 'redux';
3import { MessageSender } from './MessageSender';
4import reducer from './reducer';
5
6const store = createStore(reducer);
7class App extends React.Component {
8 render() {
9 <MessageSender store={store} />
10 };
11};
1// MessageSender.js
2import { sendMsg } from './actions';
3// ...
4this.props.store.dispatch(sendMsg(msg))
5// ...
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// MessageSender.container.js
2import { connect } from 'react-redux';
3import { sendMsg } from './actions';
4import MessageSender from './MessageSender';
5
6const mapDispatchToProps = {
7 sendMsg
8};
9
10export default connect(null, mapDispatchToProps)(MessageSender);
11
12// MessageSender.js
13// ...
14this.props.sendMsg(msg);
15// ...
If we want to dispatch many actions in a single method, we can do that as shown below:
1import { connect } from 'react-redux';
2import { sendMsg, navigateTo } from './actions';
3
4const mapDispatchToProps = dispatch => ({
5 sendMsg: msg => {
6 dispatch(sendMsg(msg));
7 dispatch(navigateTo({ routeName: 'myMsgList' }));
8 }
9});
10
11export default connect(null, mapDispatchToProps)(MessageSender);
1// MessageSender.js
2// ...
3this.props.sendMsg(msg);
4// ...
5});
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:
1npm install redux-saga
Below is the JS for our component:
1// my-sagas.js
2import { put } from 'redux-saga/effects';
3import { sendMsg, setLoading, navigateTo } from './actions';
4
5export function* sendMsg(action) {
6 yield put(setLoading('sendMsgPage', true));
7 yield put(sendMsg(action.payload.message));
8 yield put(navigateTo({routeName: 'myMsgList'}));
9 yield put(setLoading('sendMsgPage', false));
10}
Another example of making async calls using saga is shown below.
We'll create a basic Saga:
1export function* heySaga() {
2 console.log('Hi there from a basic Sagas example!')
3}
Below is our main.js:
1// ...
2import { createStore, applyMiddleware } from 'redux'
3import createSagaMiddleware from 'redux-saga'
4
5// ...
6import { heySaga } from './sagas'
7
8const sagaMiddleware = createSagaMiddleware()
9const store = createStore(
10 reducer,
11 applyMiddleware(sagaMiddleware)
12)
13sagaMiddleware.run(heySaga)
14
15const action = type => store.dispatch({type})
16
17// rest
And below is our counterexample showing async calls:
1const MyCounter = ({ value, onInc, onDec, onIncAsync }) =>
2 <div>
3 <button onClick={onIncAsync}>
4 Increase after 1 second
5 </button>
6 {' '}
7 <button onClick={onInc}>
8 Increase
9 </button>
10 {' '}
11 <button onClick={onDec}>
12 Decrease
13 </button>
14 <hr />
15 <div>
16 You clicked on the button {value} times
17 </div>
18 </div>
We'll then connect the onIncAsync of our React Component to Store action. We'll update main.js module as shown below:
1function render() {
2 ReactDOM.render(
3 <MyCounter
4 value={store.getState()}
5 onInc={() => action('INC')}
6 onDec={() => action('DEC')}
7 onIncAsync={() => action('INC_ASYNC')} />,
8 document.getElementById('root')
9 )
10}
Unlike in redux-thunk, our component here dispatches a plain object action. We'll add the following code in sagas.js:
1import { put, takeEvery } from 'redux-saga/effects'
2const delay = (ms) => new Promise(response => setTimeout(response, ms))
3
4// ...
5
6// Our Saga worker would perform the async inc task
7export function* incAsync() {
8 yield delay(2000)
9 yield put({ type: 'INC' })
10}
11
12// Our Saga watcher would spawn a new incAsync task on each INC_ASYNC
13export function* watchIncAsync() {
14 yield takeEvery('INC_ASYNC', incAsync)
15}
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:
1import { put, takeEvery, all } from 'redux-saga/effects'
2
3const delay = (ms) => new Promise(response => setTimeout(response, ms))
4
5function* heySaga() {
6 console.log('Hi there from a basic Sagas example!');
7}
8
9function* incAsync() {
10 yield delay(2000)
11 yield put({ type: 'INC' })
12}
13
14function* watchIncAsync() {
15 yield takeEvery('INC_ASYNC', incAsync)
16}
17
18// you can see that we just export the mainSaga
19// This is the single entry point to start all our sagas immediately
20export default function* mainSaga() {
21 yield all([
22 heySaga(),
23 watchIncAsync()
24 ])
25}
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// ...
2import mainSaga from './sagas'
3
4const sagaMiddleware = createSagaMiddleware()
5const store = createStore(
6 reducer,
7 applyMiddleware(sagaMiddleware)
8)
9sagaMiddleware.run(rootSaga)
10
11const action = type => store.dispatch({type})
12
13// rest
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@connect(store => ({ myReactStore: store.myReactStore }))
2class MyReactContainer extends Component {
3 onSomeTaskStart = (payload) => {
4 this.props.dispatch(MyActionTypes.SOME_TASK_START, payload);
5 }
6 onSomeTaskEnd = (payload) => {
7 this.props.dispatch(MyActionTypes.SOME_TASK_END, payload);
8 }
9 render() {
10 return (
11 <MyComponent
12 onSomeTaskStart={this.onSomeTaskStart}
13 onSomeTaskEnd={this.onSomeTaskEnd}
14 />
15 }
16}
Below is our component code:
1class MyReactComponent extends Component {
2 static propTypes = {
3 onSomeTaskStart: PropsTypes.func,
4 onSomeTaskEnd: PropTypes.func,
5 }
6 render() {
7 return (
8 <div>
9 // we'll have the view code here
10 </div>
11 )
12 }
13}
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.
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// MsgSenderPage.js
2import { bindActionCreators } from 'redux';
3import { connect } from 'react-redux';
4import * as actions from './actions';
5
6class MsgSenderPage extends React.Component {
7 constructor(props) {
8 super(props);
9 const { dispatch } = props;
10 this.boundedActions = bindActionCreators(actions, dispatch);
11 }
12
13 render() {
14 return <MsgSending {...this.boundedActions} />;
15 }
16}
17
18export default connect()(MsgSenderPage);
We can also use bindActionCreators and extend our props as shown below:
1import { bindActionCreators } from 'redux';
2// ...
3const mapDispatchToProps = dispatch => ({
4 ...bindActionCreators(
5 {
6 sendMsg,
7 deleteMsg,
8 },
9 dispatch,
10 ),
11 someOtherService, // this does not have to be wrapped in dispatch
12})
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.
Explore these React and Redux courses from Pluralsight to continue learning: