Immutability means that variables cannot be modified once they have been set with data, which prevents unwanted changes from occurring.
The concept of immutability is strictly connected with pure functions. In pure functions:
The major benefits of pure functions is that they are easy to test as their output is predictable which produces more robust code. As objects and arrays cannot be modified after they are created, any new change is stored into new copies of the same data structures as follows:
The following are four important aspects of immutability:
Redux is a JavaScript library designed to manage applications state and it can be useful to better understand the idea of immutability. In well written Redux applications, changes always occur following predictable patterns. Although all JavaScript arrays and objects can be modified at any time, Redux expects that all updates are done immutably. Moreover, each Redux application has a single store that contains all application data (state). A basic Redux application will contain:
The following example is a Redux application designed to mimic a cluster management application with the following functionalities:
First of all, the reader should make sure to include the Redux library as follows:
<HEAD>
<SCRIPT TYPE="text/javascript" SRC="./redux.min.js"></SCRIPT>
</HEAD>
Next, a basic UI for the application is built as follows which should all go inside the <BODY> tag:
ADD NODES:
<BR>
<INPUT TYPE="text" PLACEHOLDER="Add new node" ID="nodename" />
<BUTTON ID="add">ADD</BUTTON>
<BR><BR>
REMOVE NODES:
<BR>
<SELECT STYLE="width:180px" NAME="nodes" SIZE="1" ID="nodes">
</SELECT>
<BUTTON ID="remove">REMOVE</BUTTON>
<BR><BR>
<BUTTON ID="addJob">ASSIGN JOB</BUTTON>
<BUTTON ID="softwareInstall">INSTALL SOFTWARE</BUTTON>
<BR><BR>
<P ID='pJob'>ASSIGNED JOBS:</P>
<P ID='sUpdate'>UPDATE PROCESS</P>
From now on, all code presented in this tutorial will have to be enclosed into an Immediately Invoked Function Expression inside the HTML <SCRIPT> tag. Immediately Invoked Function Expression look like this:
(function () {
//All Redux code goes here....
})();
Next, let’s define some constants to make the code more readable:
const CLUSTER = {
ADD_TO_CLUSTER : 'ADD_TO_CLUSTER',
REMOVE_FROM_CLUSTER : 'REMOVE_FROM_CLUSTER'
};
const STATUS = {
ASSIGNED : 'ASSIGNED',
UPDATED : 'UPDATED'
};
Let’s create the actions:
const newNode = function(text) { return { type : CLUSTER.ADD_TO_CLUSTER, text }; }
const deleteNode = function(text) { return { type : CLUSTER.REMOVE_FROM_CLUSTER, text }; }
const assignJob = function(id) { return { type : STATUS.ASSIGNED, id }; }
const softwareUpdate = function(success) { return { type : STATUS.UPDATED, success }; }
Next, let’s create two reducers:
const nodeManager = function(state = [], action) {
switch(action.type) {
case 'ADD_TO_CLUSTER': {
//Create a state with a new node
return [ ...state, { text: action.text, jobs: { pid: [1], running: false } } ]
}
case 'REMOVE_FROM_CLUSTER': {
//Create a copy of the state without the node to be deleted
return state.filter(({ text }) => text !== action.text);
}
//This will assign jobs to nodes
case 'ASSIGNED': {
return state.map((nodeManager) => {
//This passes a new object, old data and new data
return Object.assign({}, nodeManager, {
//concat() does not modify the array, it makes a copy!
jobs: { pid: nodeManager.jobs.pid.concat(action.id), running: true }
})
});
}
default: { return state }
}
}
//This cannot see states created by nodeManager!!!
const clusterManager = function(state = [], action) {
switch(action.type) {
case 'UPDATED': {
return [ ...state, { success: action.success } ]
}
default: { return state }
}
}
The two reducers need to be combined and they will be sharing the same store:
const reducer = Redux.combineReducers({ nodeManager, clusterManager });
const store = Redux.createStore(reducer);
Let’s assign the actions to the elements of UI so that they can be dispatched:
//Add actions to buttons
document.getElementById('add').onclick = function(ev) {
store.dispatch(newNode(document.getElementById('nodename').value));
document.getElementById('nodename').value = null;
}
document.getElementById('remove').onclick = function(ev) {
const sel = document.getElementById('nodes');
store.dispatch(deleteNode(sel.options[sel.selectedIndex].value));
}
document.getElementById('addJob').onclick = function(ev) {
const jobID = Math.floor((Math.random() * 1000) + 1);
store.dispatch(assignJob(jobID));
}
//Install software
document.getElementById('softwareInstall').onclick = function(ev) {
const success = Math.floor((Math.random() * 2));
store.dispatch(softwareUpdate(success));
}
The remaining code will manage the UI updates:
/*
The state can only be changed by emitting an action. The state tree is never mutated directly
Reducers take the current state tree and an action as arguments and returns the resulting state tree
*/
let prevState = store.getState();
const container = document.getElementById('nodes');
store.subscribe(() => {
const state = store.getState();
if(prevState === state) { return; }
//Add a node to UI as the nodeManager store has been increasing
if(prevState.nodeManager.length < state.nodeManager.length) {
addToUI(state.nodeManager[state.nodeManager.length - 1].text);
}
//Delete a node from UI as the nodeManager store has been decreasing
if(prevState.nodeManager.length > state.nodeManager.length) {
document.getElementById('nodes').innerHTML = null;
state.nodeManager.forEach(element => addToUI(element.text));
}
//An update has just run as the clusterManager store has been increasing
if(prevState.clusterManager.length < state.clusterManager.length) {
runUpdate(state.clusterManager[state.clusterManager.length - 1].success);
}
//DEBUG START//
console.log(state);
const output = 'ASSIGNED JOBS:<BR>' + state.nodeManager[state.nodeManager.length - 1].jobs.pid.toString();
document.getElementById('pJob').innerHTML = output;
//DEBUG END//
prevState = state;
});
const addToUI = function (computerName = null) {
var sel = document.getElementById('nodes');
var opt = document.createElement('option');
opt.value = computerName;
opt.innerHTML = '💻 ' + computerName;
sel.appendChild(opt);
}
const runUpdate = function (success) {
success = success === 0 ? 'OK' : 'FAIL';
const output = 'UPDATE PROCESS:<BR>' + new Date().toLocaleString() + ' >> ' + success;
document.getElementById('sUpdate').innerHTML = output;
}
The reader should run the application while having the JavaScript debugging tools available as the application state will be available on the JavaScript console. In the console the reader should pay attention to the differences between the two reducers: ‘clusterManager’ and ‘nodeManager’. The reader should try moving code around to make sure reducers are not able to access each other states.