Immutability in React and Redux

https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/
1. Invariance and side effects

1. Unchanging: Continuously create new values to replace old values

2. Invariance rules:

(1) A pure function must always return the same value when given the same input

(2) Pure functions cannot have any side effects

3. Side effects: modifying things outside the scope of the direct function

Change/modify the parameters (properties of) passed in

Modify any other state outside the function, such as global variables

Make API calls

console.log()

Math.random()

4. Array methods that change the array: push, pop, shift, unshift, sort, reverse, splic

If you want to perform immutable operations on an array, you can copy the array first and then operate on the copy.

5. Pure functions can only call other pure functions

2. React prefers immutability

1. Immutability and PureComponent

React components that inherit the function and class types of React.Component will be re-rendered when the parent component is re-rendered or setState is used.

A class that inherits React.PureComponent will only re-render when its state is updated or its props are updated.

When passing props into PureComponent, you need to ensure immutable updates, because if you directly modify the internals or properties, but the reference remains unchanged, the component cannot notice that it has changed and will not re-render.

2. Reference equality in JS

Objects and arrays are stored in memory. When a variable is reallocated, the variable will point to a new memory address, but if only the internal structure of the variable is changed, the variable still points to the same address, and the content in the address changes.

When using === to compare objects and arrays, the essence is to compare reference addresses, that is, “reference equality”.

When you modify an object, the object’s contents are modified, but its reference is not changed. When you assign one object to another object pointing to the same memory location, operations on the second object will also affect the value of the first object.

Why not check for equality in depth: slow, time complexity O(N)

Comparing references: O(1), no matter how complex the object is internally

3.const only prevents the reassignment of references, but does not prevent the object content from being changed.

4. Thinking: The React framework considers performance. In order to ensure performance in complex scenarios, it does not compare the internal values of the objects one by one during comparison, but compares the reference addresses to ensure that the time complexity of each comparison is O(1 ). Therefore, starting from this design idea, when updating the State, the new value is returned based on the old value (the reference address changes), instead of directly modifying the content of the object through object.property=xxx. Therefore, when most languages modify the properties of an object, they modify it through object.property=xxx. React returns a new value based on the old value.

3. Update state in Redux (main content, the above is to explain this)

1. Reducers are required to be pure functions and cannot directly modify the state: accept state and action, and return a new state based on the old state. The above section explains mutability and immutability. The following explains how to update state immutably.

  1. Object spread operator…: Creates a new object or array containing exactly the same contents as another object or array

3. Update state: The incoming this.setState() object will be shallowly merged.

In the reducer of redux:

(1) Update ordinary state

return {
…state,
(updates here)
}

(2) When updating the top-level properties of an object, you need to copy the existing state, expand the object through…, and then add the properties and their values that you want to change.

function reducer(state, action) {
/*
State looks like:

state = {
  clicks: 0,
  count: 0
}

*/

return {
…state,
clicks: state.clicks + 1,
count: state.count – 1
}
}

(3) Update the objects in the object. When the part you want to update is in the deep layer, you need to expand each level and create a copy.

function reducer(state, action) {
/*
State looks like:

state = {
  house: {
    name: "Ravenclaw",
    points: 17
  }
}

*/

// Two points for Ravenclaw
return {
…state, // copy the state (level 0)
house: {
…state.house, // copy the nested object (level 1)
points: state.house.points + 2
}
}

(4) Update the object through key

function reducer(state, action) {
/*
State looks like:

const state = {
  houses: {
    gryffindor: {
      points: 15
    },
    ravenclaw: {
      points: 18
    },
    hufflepuff: {
      points: 7
    },
    slytherin: {
      points: 5
    }
  }
}

*/

// Add 3 points to Ravenclaw,
// when the name is stored in a variable
const key = “ravenclaw”;
return {
…state, // copy state
houses: {
…state.houses, // copy houses
[key]: { // update one specific house (using Computed Property syntax)
…state.houses[key], // copy that specific house’s properties
points: state.houses[key].points + 3 // update its points property
}
}
}

(5) Add an Item to the array

Adding Item to the front/back of the array through Array.prototype.unshift and push will change the array. We hope to add Item in an immutable way (object expansion and copying, or copying through .slice and then pushing)

function reducer(state, action) {
/*
State looks like:

state = [1, 2, 3];

*/

const newItem = 0;
return [ // a new array
newItem, // add the new item first
…state // then explode the old state at the end Add to front
];

return [ // a new array
newItem, // add the new item first
…state // then explode the old state at the end and add it to the end
];

//Or copy the array through .slice and push to the end
function reducer(state, action) {
const newItem = 0;
const newState = state.slice();

newState.push(newItem);
return newState;

(6) Update the items in the array through map

Use .map to traverse each item, find the item to be modified, use the return value as the new value of the item, and finally map returns a new array. If you need to filter, use .filter

function reducer(state, action) {
/*
State looks like:

state = [1, 2, "X", 4];

*/

return state.map((item, index) => {
// Replace “X” with 3
// alternatively: you could look for a specific index
if(item === “X”) {
return 3;
}

// Leave every other item unchanged
return item;

});
}

(7) Update objects in the array

Traverse each item through .map, find the object to be modified, copy the object and modify the copy, and return it as a new object. Finally, map returns a new array. If you need to filter, use .filter

function reducer(state, action) {
/*
State looks like:

state = [
  {
    id: 1,
    email: '[email protected]'
  },
  {
    id: 2,
    email: '[email protected]'
  }
]

Action contains the new info:

action = {
  type: "UPDATE_EMAIL"
  payload: {
    userId: 2, // Peter's ID
    newEmail: '[email protected]'
  }
}

*/
//What is returned through map is a new array
return state.map((item, index) => {
// Find the item with the matching id
if(item.id === action.payload.userId) {
// Return a new object After modification, a new object is returned
return {
…item, // copy the existing item Copy the parts of this object that do not need to be modified
email: action.payload.newEmail // replace the email addr
}
}

// Leave every other item unchanged
return item;

});
}

(8) Insert an item in the middle of the array

The .splice function will insert an item but change the array. You can try slice to copy the array first, and then use splice to insert the item, or copy the elements before the new item, insert the new item, and then copy the elements after the item.

function reducer(state, action) {
/*
State looks like:

state = [1, 2, 3, 5, 6];

*/

const newItem = 4;

//make a copy
const newState = state.slice();

// insert the new item at index 3
newState.splice(3, 0, newItem)

return newState;

/*
// You can also do it this way:

return [ // make a new array
…state.slice(0, 3), // copy the first 3 items unchanged
newItem, // insert the new item
…state.slice(3) // copy the rest, starting at index 3
];
*/
}

(9) Update the item in the array by index

Use map to find this item and return the new value

function reducer(state, action) {
/*
State looks like:

state = [1, 2, "X", 4];

*/

return state.map((item, index) => {
// Replace the item at index 2
if(index === 2) {
return 3;
}

// Leave every other item unchanged
return item;

});
}

(10) Delete an item from the array

Through filter, pass in each item and return a new array containing only items whose judgment function returns true.

function reducer(state, action) {
/*
State looks like:

state = [1, 2, "X", 4];

*/

return state.filter((item, index) => {
// Remove item “X”
// alternatively: you could look for a specific index
if(item === “X”) {
return false;
}

// Every other item stays
return true;

});
}