Short Answer
Yes! The ES6 spread operator can be used as a replacement for immutable.js entirely, but there is one major caveat, you must maintain situational awareness at all times.
VERY Long Answer
You, and your fellow developers, will be 100% responsible for maintaining immutability, rather than letting immutable.js take care of it for you. Here's a breakdown of how you can manage an immutable state all by yourself using the ES6 'spread operator', and its various functions like filter
and map
.
The following will explore removing and adding values to an array or object in an immutable and mutated way. I log out the initialState
and newState
in each example to demonstrate if we've mutated initialState
. The reason this is important, is because Redux will not instruct the UI to re-render if the initialState
and newState
are exactly the same.
Note: Immutable.js would crash the application if you tried any of the mutated solutions below.
Remove element from Array
Immutable way
const initialState = {
members: ['Pete', 'Paul', 'George', 'John']
}
const reducer = (state, action) => {
switch(action.type){
case 'REMOVE_MEMBER':
return {
...state,
members: state.members.filter(
member => member !== action.member
)
}
}
}
const newState = reducer(
initialState,
{type: 'REMOVE_MEMBER', member: 'Pete'}
);
console.log('initialState', initialState);
console.log('newState', newState);
Mutated way
const initialState = {
members: ['Pete', 'Paul', 'George', 'John']
}
const reducer = (state, action) => {
switch(action.type){
case 'REMOVE_MEMBER':
state.members.forEach((member, i) => {
if (member === action.member) {
state.members.splice(i, 1)
}
})
return {
...state,
members: state.members
}
}
}
const newState = reducer(
initialState,
{type: 'REMOVE_MEMBER', member: 'Pete'}
);
console.log('initialState', initialState);
console.log('newState', newState);
Add element to Array
Immutable way
const initialState = {
members: ['Paul', 'George', 'John']
}
const reducer = (state, action) => {
switch(action.type){
case 'ADD_MEMBER':
return {
...state,
members: [...state.members, action.member]
}
}
}
const newState = reducer(
initialState,
{type: 'ADD_MEMBER', member: 'Ringo'}
);
console.log('initialState', initialState);
console.log('newState', newState);
Mutated way
const initialState = {
members: ['Paul', 'George', 'John']
}
const reducer = (state, action) => {
switch(action.type){
case 'ADD_MEMBER':
state.members.push(action.member);
return {
...state,
members: state.members
}
}
}
const newState = reducer(
initialState,
{type: 'ADD_MEMBER', member: 'Ringo'}
);
console.log('initialState', initialState);
console.log('newState', newState);
Update Array
Immutable way
const initialState = {
members: ['Paul', 'Pete', 'George', 'John']
}
const reducer = (state, action) => {
switch(action.type){
case 'UPDATE_MEMBER':
return {
...state,
members: state.members.map(member => member === action.member ? action.replacement : member)
}
}
}
const newState = reducer(
initialState,
{type: 'UPDATE_MEMBER', member: 'Pete', replacement: 'Ringo'}
);
console.log('initialState', initialState);
console.log('newState', newState);
Mutated way
const initialState = {
members: ['Paul', 'Pete', 'George', 'John']
}
const reducer = (state, action) => {
switch(action.type){
case 'UPDATE_MEMBER':
state.members.forEach((member, i) => {
if (member === action.member) {
state.members[i] = action.replacement;
}
})
return {
...state,
members: state.members
}
}
}
const newState = reducer(
initialState,
{type: 'UPDATE_MEMBER', member: 'Pete', replacement: 'Ringo'}
);
console.log('initialState', initialState);
console.log('newState', newState);
Merge Arrays
Immutable way
const initialState = {
members: ['Paul', 'Ringo']
}
const reducer = (state, action) => {
switch(action.type){
case 'MERGE_MEMBERS':
return {
...state,
members: [...state.members, ...action.members]
}
}
}
const newState = reducer(
initialState,
{type: 'MERGE_MEMBERS', members: ['George', 'John']}
);
console.log('initialState', initialState);
console.log('newState', newState);
Mutated way
const initialState = {
members: ['Paul', 'Ringo']
}
const reducer = (state, action) => {
switch(action.type){
case 'MERGE_MEMBERS':
action.members.forEach(member => state.members.push(member))
return {
...state,
members: state.members
}
}
}
const newState = reducer(
initialState,
{type: 'MERGE_MEMBERS', members: ['George', 'John']}
);
console.log('initialState', initialState);
console.log('newState', newState);
The above examples of mutating an array may seem like obvious bad practices to a seasoned developer, but an easy pitfall for someone new on the scene. We'd hope that any of the Mutated way code snippets would get caught in code review, but that's not always the case.
Let's talk a little bit about objects, which are more cumbersome when handling immutability on your own.
Remove from Object
Immutable way
const initialState = {
members: {
paul: {
name: 'Paul',
instrument: 'Guitar'
},
stuart: {
name: 'Stuart',
instrument: 'Bass'
}
}
}
const reducer = (state, action) => {
switch(action.type){
case 'REMOVE_MEMBER':
let { [action.member]: _, ...members } = state.members
return {
...state,
members
}
}
}
const newState = reducer(
initialState,
{type: 'REMOVE_MEMBER', member: 'stuart'}
);
console.log('initialState', initialState);
console.log('newState', newState);
Mutated way
const initialState = {
members: {
paul: {
name: 'Paul',
instrument: 'Guitar'
},
stuart: {
name: 'Stuart',
instrument: 'Bass'
}
}
}
const reducer = (state, action) => {
switch(action.type){
case 'REMOVE_MEMBER':
delete state.members[action.member]
return {
...state,
members: state.members
}
}
}
const newState = reducer(
initialState,
{type: 'REMOVE_MEMBER', member: 'stuart'}
);
console.log('initialState', initialState);
console.log('newState', newState);
Update Object
Immutable way
const initialState = {
members: {
paul: {
name: 'Paul',
instrument: 'Guitar'
},
ringo: {
name: 'George',
instrument: 'Guitar'
}
}
}
const reducer = (state, action) => {
switch(action.type){
case 'CHANGE_INSTRUMENT':
return {
...state,
members: {
...state.members,
[action.key]: {
...state.members[action.member],
instrument: action.instrument
}
}
}
}
}
const newState = reducer(
initialState,
{type: 'CHANGE_INSTRUMENT', member: 'paul', instrument: 'Bass'}
);
console.log('initialState', initialState);
console.log('newState', newState);
Mutated way
const initialState = {
members: {
paul: {
name: 'Paul',
instrument: 'Guitar'
},
ringo: {
name: 'George',
instrument: 'Guitar'
}
}
}
const reducer = (state, action) => {
switch(action.type){
case 'CHANGE_INSTRUMENT':
state.members[action.member].instrument = action.instrument
return {
...state,
members: state.members
}
}
}
const newState = reducer(
initialState,
{type: 'CHANGE_INSTRUMENT', member: 'paul', instrument: 'Bass'}
);
console.log('initialState', initialState);
console.log('newState', newState);
If you've made it down this far, congratulations! I know this has been a long winded post, but I felt it was important to demonstrate all the Mutated ways that you'll need to prevent yourself without Immutable.js. One huge advantage to using Immutable.js, beyond preventing you from writing bad code, is the helper methods, like mergeDeep
and updateIn
Immutable.JS
mergeDeep
const initialState = Immutable.fromJS({
members: {
paul: {
name: 'Paul',
instrument: 'Guitar'
},
ringo: {
name: 'George',
instrument: 'Guitar'
}
}
})
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_MEMBERS':
return state.mergeDeep({members: action.members})
}
}
const newState = reducer(
initialState,
{
type: 'ADD_MEMBERS',
members: {
george: { name: 'George', instrument: 'Guitar' },
john: { name: 'John', instrument: 'Guitar' }
}
}
);
console.log('initialState', initialState);
console.log('newState', newState);
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.2/immutable.min.js"></script>
updateIn
const initialState = Immutable.fromJS({
members: {
paul: {
name: 'Paul',
instrument: 'Guitar'
},
ringo: {
name: 'George',
instrument: 'Guitar'
}
}
})
const reducer = (state, action) => {
switch (action.type) {
case 'CHANGE_INSTRUMENT':
return state.updateIn(['members', action.member, 'instrument'], instrument => action.instrument)
}
}
const newState = reducer(
initialState,
{type: 'CHANGE_INSTRUMENT', member: 'paul', instrument: 'Bass'}
);
console.log('initialState', initialState);
console.log('newState', newState);
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.2/immutable.min.js"></script>