React Redux: View not getting updated even though mapStateToProps is - arrays

My Application and Store
Using Redux with ReactJS, I am keeping an array of objects (called results) in the store and dispatching actions that sort and manipulate it. In mapStateToProps, I return this results array, which renders the results in a list on the view.
// in Component file
export default class ResultList extends Component {
constructor(props) {
super(props);
this.renderResults = this.renderResults.bind(this);
}
renderResults(results) {
return (
results.map((result) => {
return <AnotherComponent />
})
);
}
render() {
const { results } = this.props;
return (
<div>
<ul>
{this.renderResults(results)}
</ul>
</div>
)
// in Container Component file
const mapStateToProps = (state, ownProps) => {
console.log('map state is triggered');
return {
results: state.results
};
}
The Problem
I have found that although my mapStateToProps successfully triggers to update the props to my class component, for some reason the view only updates on the FIRST time that the results array gets manipulated, but does not update on future updates to the store. (Please see update below. This is not entirely accurate.)
I have made sure that this problem is not due to mutating state in my reducers as is often the case; I have confirmed that the mapStateToProps runs every time that the store gets updated (as indicated by the console.log). The problem seems to be between the results props getting returned from mapStateToProps and the view actually rendering the results array, but I do not have visibility to see what Redux is doing under the hood.
The closest problem that someone else has had to what I am experiencing seems to be this, but I do not know how or if this fix applies to my use of a stored array: https://forums.meteor.com/t/the-state-is-correctly-mutated-and-returned-but-view-does-not-rerender/28840/5
Any help would be greatly appreciated. Thank you.
Update
I apologize, but must correct my statement above saying that there are no further updates to the view after the first time the results in the store gets updated. With further testing I have found that is not completely true. The view gets updated only when the results array is sorted according to case 1 of the below sorting function. Cases 2 and 3 are the ones that result in no update to the view. This may/may not be necessary information, but the results array gets sorted by these 3 cases in a cycle onClick in the following order: case 1, case 3, case 2.
// in reducer index.js file
case SORT_RESULTS:
return {
...state,
results: sortArr(state.results, state.sortType)
};
// sorting function
function sortArr(arr, sortType) {
let newArr = [];
switch (sortType) {
case '1':
for (let i = arr.length - 1; i >= 0; i--) {
newArr.push(arr[i]);
}
return newArr;
case '2':
newArr = arr;
newArr.sort((a, b) => {
return b.num - a.num;
});
return newArr;
case '3':
newArr = arr;
newArr.sort((a, b) => {
let id1 = a.id.toLowerCase();
let id2 = b.id.toLowerCase();
if (id1 < id2) {
return -1;
}
if (id1 > id2) {
return 1;
}
return 0;
});
return newArr;
default:
return arr;
}
}

The Solution
Here is the culprit: newArr = arr. Change this to newArr = arr.slice(), and the view will get updated on every sort.
Possible Root Cause
As to why this solution works, I invite anyone else's perspective. Here are my thoughts:
According to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice:
"The slice() method returns a shallow copy of a portion of an array into a new array object selected from begin to end (end not included)."
Copying the results array using newArr = arr seems to only copy the pointer (to the array), which is really just a reference in memory. If this statement is true, then Redux would not consider the state to have been changed at all because it is comparing the exact same pointer (state.results), even though the actual data of the array itself is confirmed to change by sorting. Copying a completely new array through slice() would involve a new pointer and data, which would be detected as a change to Redux under the hood.

Related

Why am I mutating the original array with filter method? [React]

After executing the newData[0].id = newValue I am actually updating the react initialData state. How is that possible?
Is my understanding that filter should return a new array different than the original one, also I am not ussing the setState feature so I don't understand why the state is changing.
Because arrays are mutable. it will keep the reference to the original array even after filtering.
use the spread operator to avoid mutating the original array
const data = [...newData]
data[0].id = newValue
As per the new beta docs on updating items in array
setInitialData(prev => {
// create a new array
const withReplaced = prev.map(elem => {
if (elem.id === id) {
const newVal = //set new value
// create a new updated element
return {
...elem,
id: newVal
}
} else {
// The rest haven't changed
return elem;
}
});
return withReplaced;
})
Hope it helps
you can't update the initialData,but the you can update the son of the array.And if you don't use "setData".The views won't change.

A better way to handle updating state in reducer

So I'm trying to learn react-redux and in my reducer GET_COMMENTS. I need to check first if there is already items in state.comments so I try to use if-else statement and it works. But maybe there's still a better way to handle it ?
case 'GET_COMMENTS':
let list = []
if (state.comments.length > 0) { // check if there's an item
list = state.comments // pass existing state
action.payload.data.map(comment => { // map through new data
list = [...list, comment] // dont know if this is the right way but it works
})
} else {
list = action.payload.data // if state.comments is empty directly pass new data
}
return {
...state,
comments: list,
next: action.payload.next
}
UPDATE: I decided to go with Gabriele answer as I think its the best approach. And Today I learn that .concat() method is used to join two or more arrays. This method does not change the existing arrays, but returns a new array, containing the values of the joined arrays.
I would just do
case 'GET_COMMENTS':
return ({
...state,
comments: state.comments.concat(action.payload.data),
next: action.payload.next
});
Yes it is correct. I would simplify your approach to
...
case 'GET_COMMENTS':
return {
...state,
comments: [...state.comments, ...action.payload.data]
next: action.payload.next
};
Note: I consider that action.payload.comments is a new array of comments. And initial state is { comments: [] }.

Redux: will this be the same state after I change a property of an element in state?

I am working on the reducer file which looks like this:
export default (state = [], action) => {
let index
switch (action.type){
case "ADD_QUOTE":
return [...state,action.quote]
case "REMOVE_QUOTE":
return state.filter(q=>q.id !==action.quoteId)
case "UPVOTE_QUOTE":
index = state.findIndex(quote => quote.id === action.quoteId);
let quote = state[index];
return [
...state.slice(0, index),
Object.assign({}, quote, { votes: quote.votes += 1 }),
...state.slice(index + 1)
];
default:
return state
}
}
I have two ways to implement case UPVOTE_QUOTE, the first one is in the above code, and the second one looks like this:
...
case "UPVOTE_QUOTE":
let quote = state.find(q=>q.id===action.quoteId)
quote.votes +=1
return state
...
I wonder what is the difference. And in the second situation, what I am not sure is, if I change the votes property in that one quote element in state, when I return the state, will it be the state with the new quote? And is this state the original state or a new state that refers to a new place? Very confused...
The first option looks to be correct. The second option would be mutating the state.
These links that may help explain further: Mutating Redux State Consequences and Why cant state be mutated....

Copying two-dimensional array from state in React.js

I'm attempting to build a small react app to model Conway's Game of Life. I've set up a two-dimensional array to track the state of each cell in a 10-by-10 grid.
I'm attempting to store this array in the State. At each "tick" of the game, I want to make a copy of the array, evaluate each cell, potentially giving it a new value, and then assign the copy back to state. I'm basing this off of the official React Tutorial where they use this exact approach:
handleClick(i) {
//Make a copy from state
const squares = this.state.squares.slice();
//Make some changes to it
squares[i] = 'X';
//Set state to the new value
this.setState({squares: squares});
}
My initial approach was to use slice() as in the example above. Through debugging, I discovered that this didn't work; somehow state is being changed even though I have used various methods to copy it that shouldn't make changes to it. (I understand that if I say var x = this.state.blah and x = 5 that I have changed state because blah is a reference to it)
Here is my code:
doTick = () => {
console.log("doin a tick");
console.log(this.state.squares);
//None of these approaches works
//Three different copy strategies all fail
//const newSquares = Object.assign({}, this.state.squares);
//const newSquares = [...this.state.squares];
//const newSquares = this.state.squares.slice();
const newSquares = this.state.squares.slice();
const origSquares = [...this.state.squares];
//Iterating over the array
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
newSquares[i][j] = evaluateCell(origSquares[i][j], this.countLiveNeighbors(i, j, origSquares));
//evaluateCell(origSquares[i][j], this.countLiveNeighborsAndLog(i, j, origSquares));
}
}
//this.setState({
//squares: newSquares
//});
}
Even though the setState() call is commented out, just having the assignment of newSquares[i][j] = //... is enough to somehow modify state.
Here's the code where I set up the initial array in the constructor for the Board component:
constructor(props) {
super(props);
var array = new Array(10);
for (var i = 0; i < 10; i++) {
array[i] = new Array(10).fill(false);
}
this.state = {
squares: array
};
console.log(this.state.squares);
}
I took a look here but I'm not having any trouble updating the squares based on clicks (that part of my code works fine). Various SO posts and in-person troubleshooters suggested the three different copy strategies that all produce the same problem. I also took a look here.
I'm very new to React and not very skilled in JS generally, and obviously I don't have a good handle on State. Here are my questions:
How can I make a copy of state/part of state/data in state in such a way that the copy is not a reference to state? I want to be able to change this new data without changing state (until I am ready).
Why are the methods used above NOT working properly? The documentation for slice() assures me that I'm getting a copy and not a reference.
Thanks in advance! I'm very confused.
The spread operator only does a shallow copy of the values. This means if you have any nested values in them, they will be referenced rather than copied. For instance:
const a = { field: { innerField: 'test' } };
const b = { ...a } // b === { field: { innerField: 'test' } } SAME field as a
To copy a nested array you should use deep copy methods, such as Lodash's cloneDeep or Ramda's clone
for example, with Lodash's cloneDeep:
const newSquares = _.cloneDeep(this.state.squares);

Having issues with updating state of deeply nested arrays in Redux

I've got a state in redux that looks like this:
I'm trying to update the flags of "false" of selected arrays, but have trouble grasping such deeply nested array modification in redux.
Here was my take, that gives me syntax errors:
case 'SET_FLAG':{
return {
...state,
pickedSquares: [
...state.pickedSquares,
pickedSquares[action.index]: [
...state.pickedSquares[action.index],
]
]
}
}
And I probably still need to go 2 levels deeper.
I would appreciate the help.
I recommend making a copy of pickedSquares and then modifying the copy directly. Since pickedSquares is an array, and not a JS object, you won't be able to spread and overwrite values in the manner you are currently trying.
Here is an example of a way to solve the problem based on the code you have provided:
case 'SET_FLAG':
const pickedSquaresCopy = JSON.parse(JSON.stringify(state.pickedSquares)); //make a deep copy
pickedSquaresCopy[action.index] = action.newValue;
return {
...state,
pickedSquares: pickedSquaresCopy
}
What is shown is only a modification one level deep, but you can make them arbitrarily deep by navigating the array/object levels and modifying values that need to be modified.
Maybe not the best way but works:
case "SET_FLAG": {
const newArr = state[action.payload].map(el => {
if (el === false) {
return true;
}
return el;
});
const newState = state.map((el, index) => {
if (index === action.payload) {
return newArr;
}
return el;
});
return newState;
}
I don't know how performant this will be. Maybe you can try to make a deep copy as #Henry Wood suggested and find a way doing nested changes. But with my method, you are not mutating the state directly without doing a deep copy, too.
If you like the ternary operator:
case "SET_FLAG": {
const newArr = state[action.payload].map(el =>
el === false ? true : el
);
const newState = state.map((el, index) =>
index === action.payload ? newArr : el
);
return newState;
}

Resources