Author avatar

Manujith Pallewatte

Sharing Redux Actions and Reducers

Manujith Pallewatte

  • Feb 20, 2020
  • 8 Min read
  • 14,145 Views
  • Feb 20, 2020
  • 8 Min read
  • 14,145 Views
Web Development
React

Introduction

In Redux, reducers and actions are often seen in a one-to-one mapping. For most practical purposes, this holds, since we expect the outcome of a single action to make an impact at a single point in storage. But with complex applications, the requirement arises for actions and reducers to share each other. In this guide, we will explore two such practical scenarios and production-grade solutions available to them.

To provide context for the examples, a simple form submission app is created. Given below are the form UI, the actions, and the reducer for the form.

1// SimpleForm.jsx
2
3import React, { useState } from 'react';
4import { useSelector, useDispatch } from 'react-redux';
5import { formSubmitAction } from 'formActions.js';
6
7const SimpleForm = (props) => {
8    const formData = useState({});
9    const dispatch = useDispatch();
10    
11    return (
12        <div class="myform">
13            ...
14            <button onClick={() => { dispatch(formSubmitAction(formData)) }}>Submit Form</button>
15        </div>
16    )
17}
18
19export default SimpleForm;
20
21
22// formActions.js
23import axios from 'axios';
24
25export const FORM_ACTION_REQUEST = "FORM_ACTION_REQUEST";
26export const FORM_ACTION_SUCCESS = "FORM_ACTION_SUCCESS";
27export const FORM_ACTION_ERROR = "FORM_ACTION_ERROR";
28
29export function formActionRequest(){
30    return {
31        type: FORM_ACTION_REQUEST
32    }
33}
34
35export function formActionSuccess(result){
36    return {
37        type: FORM_ACTION_SUCCESS,
38        payload: result,
39        message: "Form Action Success!"
40    }
41}
42
43export function formActionError(error){
44    return {
45        type: FORM_ACTION_SUCCESS,
46        error: error
47    }
48}
49
50export const formAction = (formData) => {
51    return async function(dispatch) {
52        dispatch(formActionRequest());
53        try{
54            let response = (await axios.post("http://yourapi.com/form", formData)).data;
55            dispatch(formActionSuccess(response));            
56        }catch(error){
57            dispatch(formActionError(response.error));
58        }
59    }
60}
61
62//formReducer.js
63
64const initState = {
65    responseData: {}
66};
67
68export function formReducer(state = initState, action){
69    // reducer is kept blank since we do not have anything specific
70    // for the moment
71    return state;
72}
javascript

Throughout this guide, we will refer to the above components to clarify the examples.

Sharing a Single Action Between Multiple Reducers

Assume we are building a complex web app that provides feedback to user actions by means of popup notifications. For example, when the user submits a form, a notification pops up to either confirm the success of the submission or report any errors.

While this sounds trivial at first, a certain complication occurs in implementing with React and Redux. Logically, the Redux action—let's say formAction—should be able to do the following:

  • On dispatching the action, communicate the user input to the API
  • On success, direct the API response data to the relevant reducer to be processed
  • On success, show a confirmation popup notifying the user that the action was a success
  • On error, direct the API error data to the relevant reducer
  • On error, show an error notification to the user notifying the relevant error

The first solution that might come to mind would be to have separate confirmation and error storage in the relevant reducer and have a NotificationComponent listening to these. Below is an outline of such a reducer.

1//formReducer.js
2
3// following shows how notification data can be kept inside the 
4// form reducer itself
5
6import { FORM_ACTION_SUCCESS, FORM_ACTION_ERROR} from 'formActions.js';
7
8const initState = {
9    error: null,
10    message: null
11};
12
13export function formReducer(state = initState, action){
14    switch(action.type){
15        case FORM_ACTION_SUCCESS:
16            state.message = action.message;
17            return state;
18        case FORM_ACTION_ERROR:
19            state.error = action.error;
20            return state;
21    }
22}
javascript

Say we need to have five other such reducers corresponding to various elements of our app. We would end up having error and notification storage defined in each reducer. Further, the NotificationComponent would need to be updated with each new reducer. Ideally, we need a separation of concerns. Thus, we need a pattern where our actions, when dispatched, are caught by multiple reducers: (1) the action's intended reducer, and (2) the notification reducer. In Centralized Error Handing with React and Redux, we discussed how to implement such centralized solutions for error handling. Since implementing notification is only a generalized solution of the above, I will leave you to refer to the above guide which provides more in-depth details on implementation.

Sharing an Action Within Another Action

Another common occurrence is the need to dispatch multiple Redux actions together. For example, let's say that in the above-discussed form, we need to disable the submit button when clicked by the user (to prevent compulsive triggering of the button by the user), and re-enable the button when the API response is received. There are two ways this could be handled.

First, we could use the same logic as before, where we have a UI reducer for the button that listens to a specific set of actions. We include the formActionRequest to the set of listening actions and disable the button when it is dispatched. Then we listen to formActionSuccess and formActionError actions and enable the button when either of those is dispatched. While this is a sound solution, it greatly reduces code readability. Unlike the notification system, the button is not a central requirement, and it doens't need to be handled this way.

Alternatively, we can use separate UI actions declared as disableSubmitButton and enableSubmitButton. And in our formAction, we first dispatch disableSubmitButton before the API request, and on receiving the response from the API we dispatch enableSubmitButton.

1// uiActions.js
2export const ENABLE_SUBMIT_BUTTON = "ENABLE_SUBMIT_BUTTON";
3export const DISABLE_SUBMIT_BUTTON = "DISABLE_SUBMIT_BUTTON";
4
5export function enableSubmitButton(){
6    return {
7        type: ENABLE_SUBMIT_BUTTON
8    }
9}
10
11export function disableSubmitButton(){
12    return {
13        type: DISABLE_SUBMIT_BUTTON
14    }
15}
16
17// uiReducer.js
18
19// this reducer is responsible for UI changes in the form
20// note that the actual efficiency comes with the ability to separate app logic
21// from the UI logic by splitting these into different reducers
22import { ENABLE_SUBMIT_BUTTON, DISABLE_SUBMIT_BUTTON} from 'uiAcions.js';
23
24const initState = {
25    formSubmit: {
26        enabled: true
27    }
28};
29
30export function uiReducer(state = initState, action){
31    switch(action.type){
32        case ENABLE_SUBMIT_BUTTON:
33            state.formSubmit.enabled = true;
34            return state;
35        case DISABLE_SUBMIT_BUTTON:
36            state.formSubmit.enabled = false;
37            return state;
38    }
39}
javascript

Now we modify the formAction to utilize the above actions.

1import { enableSubmitButton, disableSubmitButton } from 'uiActions.js';
2
3export const formAction = (formData) => {
4    return async function(dispatch) {
5        dispatch(formActionRequest());
6        dispatch(disableSubmitButton())
7        try{
8            let response = (await axios.post("http://yourapi.com/form", formData)).data;
9            dispatch(formActionSuccess(response));            
10        }catch(error){
11            dispatch(formActionError(response.error));
12        }finally{
13            dispatch(enableSubmitButton())
14        }
15    }
16}
javascript

As shown above, this greatly improves the code readability and prevents the need for maintaining an "action set" on the UI reducer side to listen on. Although the above snippet only demonstrates the usage of it, the actual power of it comes in complex scenarios where multiple such actions need to be dispatched within a single action.

Conclusion

In this guide, we explored beyond the one-to-one mapping concept of Redux actions and reducers. While one-to-one mapping works well in a majority of cases, we explored two practical scenarios where sharing actions and reducers can be more robust than replicating logic in several places.