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.
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:
Each Action is a simple function that returns a JSON object containing two things:
The Action object will be sent from React component to the Reducer using a dispatch function.
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 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.
function addTag(tag){
return {
type: tagConstants.ADD_TAG,
payload: {
tag: tag
}
}
}
Let's say we wish to add a new tag to our recipe. We need to implement the following flow:
In terms of code, we will have a look at three files:
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.
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.
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);
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.
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));
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.
In the code below, you can see the Actions for the "Chip" (tag) component
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,
};
In the code below, you can see the Constants defined for the Actions detailed above. The constants are also grouped using an enum.
const tagConstants = {
CHANGE_TAG: 'CHANGE_TAG',
DELETE_TAG: 'DELETE_TAG',
ADD_TAG: 'ADD_TAG',
};
export {
tagConstants,
};
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.
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;
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:
For complete API details, please refer to their website.
This tutorial provided the basics details of the following elements:
Created: July 29th, 2018 Updated: July 29th, 2018