Redux Store - Reducers, Actions, Action Constants and Immutablejs

In this part of the tutorial, we will discuss the Redux Store and its usage in a React applications. The proposed version of the Redux Store will use Immutablejs library. Due to this, the tutorial will contain information regarding Redux Store as well as Immutablejs.

A Redux Store will store the complete state of a React application. The store may contain several sections, each section will be kept up to date using a Reducer. Any change in the store itself will notify React to make the proper changes to the view keeping it up to date. Once we generate all the Reducers, we can combine them together using combineReducers() to create our complete React application state.

This separation of tasks, where the Redux Store handles state, and React Components handle the view make the application much simpler to code, debug, and reason with.

Redux Reducers

Each Reducer found in the application will define a new part of the store. In the reducer file, we will define everything related to it:

  1. Initial Reducer state.
  2. Reducer function which contains a switch statement, following each case, we will handle a specific request (denoted as Action).

Redux Actions

Each Action is a simple function that returns a JSON object containing two things:

  1. Metadata - Information regarding the Action.
  2. Payload - the data itself that needs to be added to the React state.

The Action object will be sent from React component to the Reducer using a dispatch function.

Metadata

Metadata information provide needed information for successful handling of the Action itself. If the Action, for example, is to add a recipe title to recipe object, then the meta data type will be "ADD_ITEM", and the collection name will be "RECIPE".

Payload

Payload contents consist of the data themselves, the data that will modify the store.

In the following example, we will have a look at the addTag action used. The addTag action contains meta data in terms of type of the action, and the payload contains the tag data itself. This function is defined in the Actions file.

In [ ]:
function addTag(tag){
  return {
    type: tagConstants.ADD_TAG,
    payload: {
      tag: tag
    }
  }
}

Dispatching Actions to Reducers

Let's say we wish to add a new tag to our recipe. We need to implement the following flow:

  1. Call an add tag event at React Component - once the user presses Enter to add a new tag.
  2. Connect add tag event with add tag Action at React Container - this will fire the addTag action following the firing of the add tag event.
  3. Handle add tag action at Redux Reducer. - to update the store with the new tag.

In terms of code, we will have a look at three files:

  1. AddRecipe.js - the AddRecipe Component, and its onKeyPress event. Here, "handleAdditionTag" event handler function is executed once this event occurs.
  2. AddRecipePage.js - the AddRecipe Container, contains the implementation of handleAdditionTag, which will dispatch the "addTag" Action.
  3. Reducer.js - the addRecipePageReducer, where received Actions are handled via switch cases.

The result of these three steps is updating the relevant part of the store that contains the recipe tags, afterwards, React will automatically update the view.

AddRecipe.js

In the partial file below, there is a TextField item (Material-UI). This item has a properly called onKeyPress, which will launch handleAdditionTag() function once the event occurs.

In [ ]:
const AddRecipe = (props) => {
  return (
    <TextField
      className={props.classes.field}
      label="New Tag"
      margin="normal"
      value={props.tagTextFieldValue}
      onChange={e => props.handleOnChangeTag(e.target.value)}
      onKeyPress={e => props.handleAdditionTag(e.key, e.target.value)}/>
  )
};

export default withStyles(styles)(AddRecipe);

AddRecipePage.js

In the partial file below, we pair each event handler (handleAdditionTag) with an Action (addTag) using dispatch function. In handleAdditionTag we check whether the key pressed is either Enter key or comma key, and only then we dispatch the appropriate Action (addTag).

Using mapStateToProps we can propagate specific state down the React component tree, and using mapDispatchToProps we can pair specific Actions with their respective event handlers.

Finlly, we connect both, the properties, and the dispatch using connect.

In [ ]:
const mapStateToProps = (state, ownProps) => {
  return {
    tagTextFieldValue: state.getIn([recipeGeneralConstants.ADD_RECIPE_PAGE, 'tagTextFieldValue']),
  }
};

const mapDispatchToProps = (dispatch) => {
  return {
    handleAdditionTag: (keyCode, tag) => {
      if (keyCode === 'Enter' || keyCode === ',')
        dispatch(tagActions.addTag(tag));
    },
      handleOnChangeTag: (tag) => {
      dispatch(tagActions.changeTag(tag));
    },

    handleDeleteTag: (tag) => {
      dispatch(tagActions.deleteTag(tag))
    },
  }
};

export default withTheme()(connect(mapStateToProps, mapDispatchToProps)(AddRecipeCleaner));

Action Definition and Their Constants

We define an Action for each event we wish to handle. In our case, we wish to dispatch addTag Action once onKeyPress event is fired for TextField component. Since several Actions can be related together, e.g. addTag, deleteTag, modifyTag, we can group them together using enums. This adds order and clarity to the code.

Since each Action has a type property, we also define constants that detail the Action type. For example, addTag Action will be of type ADD_TAG of some value (in our case is "ADD_TAG"). We also group the constants together of their respective Actions.

Actions.js

In the code below, you can see the Actions for the "Chip" (tag) component

In [ ]:
const tagActions = {
  changeTag,
  deleteTag,
  addTag,
};


function changeTag(tag){
  return {
    type: tagConstants.CHANGE_TAG,
    payload: {
      tag: tag,
    },
  }
}

function deleteTag(tag){
  return {
    type: tagConstants.DELETE_TAG,
    payload: {
      tag: tag,
    },
  }
}

function addTag(tag){
  return {
    type: tagConstants.ADD_TAG,
    payload: {
      tag: tag
    }
  }
}

export {
  tagActions,
};

Constants.js

In the code below, you can see the Constants defined for the Actions detailed above. The constants are also grouped using an enum.

In [ ]:
const tagConstants = {
  CHANGE_TAG: 'CHANGE_TAG',
  DELETE_TAG: 'DELETE_TAG',
  ADD_TAG: 'ADD_TAG',
};

export {
  tagConstants,
};

Reducer.js

Once the action is dispatched from the event handler, it will be received at the Reducer. Inside the reducer it will be handled. The reducer contains a switch clause on the Action type, and following the type, we handle it as needed.

The code below contains part of the AddRecipe reducer relevant to the addTag action. In each case we check the Action type (meta data), and inside the case we change the store as required. In case of addTag action, the case check is of ADD_TAG type.

In [ ]:
const initialState = fromJS({
  _id: '',
  lastSaved: '',
  title: '',
  ingredients: [],
  instructions: [],
  tags: [],
  nextTagKey: 0,
  tagTextFieldValue: '',
});

const addRecipePageReducer = (state = initialState, action) => {
  switch (action.type) {
    case tagConstants.CHANGE_TAG:
      return  state.set('tagTextFieldValue', fromJS(action.payload.tag));
    case tagConstants.DELETE_TAG:
      return state.update(tagConstants.TAG_LIST, l => l.filter(tag => tag.get('key')!==action.payload.tag.key));
    case tagConstants.ADD_TAG:
      let tag = {
        key: "tag_" + state.get('nextTagKey'),
        label: action.payload.tag,
      };
      state = state.set('tagTextFieldValue', fromJS(''));
      state = state.set('nextTagKey', fromJS(state.get('nextTagKey') + 1));
      return state.update(tagConstants.TAG_LIST, l => l.push(fromJS(tag)));
    default:
      return state;
  }
};

export default addRecipePageReducer;

Immutablejs Redux Store

Since the store is immutable, each store mutation will result in a new version of the store object. This requires using Immutablejs mutation API, which is a bit different than traditional object mutation APIs:

  1. fromJS(), toJS() - used to convert any JSON object to Immutablejs object and any Immutablejs object to a JSON object.
  2. get() - used to get an object property value. Example: state.get('nextTagKey')
  3. set() - used to change a value of an object property. Example: state.set('tagTextFieldValue', fromJS(''));
  4. update() - provides the ability to execute an arrow function on the elements of an object property. Example: state.update(tagConstants.TAG_LIST, l => l.push(fromJS(tag)));
  5. getIn(), setIn(), updateIn(), same as set and update, but allows accessing nested objects of property names and property indexes. Instead of providing property name, we provide it with a list of property names/property indexes denoting the path from the root of the object to the requested property. Example: state.getIn([properyName1, index1, properyName2,index2, index3, propertyName3,...]);

For complete API details, please refer to their website.

Final Words

This tutorial provided the basics details of the following elements:

  1. Definding Actions, and Action Constants.
  2. Connecting event handlers with their respective Actions.
  3. Handling dispatched actions in the Reducer and updating the state.
  4. Using Immutablejs in the Redux Store, and some details regarding its mutative API.

Created: July 29th, 2018 Updated: July 29th, 2018