React State remains immutable for states represented with lists - reactjs

I have a state represented by two arrangements. I must, at certain events, update one of them. The status update function is actually executed, but the status does not change, it remains with the same values before it is invoked. The status update does occur when I refresh the page, but not when the events that invoke the SetState occur.
onUpdateNivel = (pos, level) => {
this.setState(state => {
const listaSensor = state.listaSensor;
const list = state.listaNivel.map((item, j) => {
if (j === pos) {
return level;
} else {
return item;
}
});
var sum = 0;
for (var i = 0; i < list.length; i++) {
sum = sum + list[i];
}
return {
list,
listaSensor,
sum
};
});
};

You assigned wrong variable.
Your state has
{listaSensor, listaNivel, sum}
But when you use setState, the return object is
{
list,
listaSensor,
sum
}
See the difference.

The problem was the way the state was returning. The right way is this:
return {
LevelList: LevelList,
SensorList: SensorList,
sum: sum
};

Related

What is the reasoning behind "The final argument passed to useCallback changed size between renders ..." warning

Opening this question here, because StackOverflow is listed as a recommended place for asking React-related questions in React docs.
I am looking for a reasoning behind throwing a The final argument passed to useCallback changed size between renders. The order and size of this array must remain constant. warning.
After looking into React code it looks like React does not properly compare prevDeps and nextDeps arrays when they have different lengths.
The comparison function looks like this (some checks were omitted for brevity):
function areHookInputsEqual(prevDeps,nextDeps) {
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
Which means:
areHookInputsEqual( ['a','b'], ['c','d'] ) === false - correct
areHookInputsEqual( ['a','b','c'], ['a','b'] ) === true - wrong
areHookInputsEqual( ['a','b'], ['a','b','c'] ) === true - wrong
areHookInputsEqual( [], ['a'] ) === true - wrong
areHookInputsEqual( ['a'], [] ) === true - wrong
Why not to write this function as following and remove warning from the codebase?
function areHookInputsEqual(prevDeps,nextDeps) {
if (prevDeps.length !== nextDeps.length) {
return false;
}
for (let i = 0; i < prevDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
The use case which led to this question
We have a custom hook like this:
function useLoadMessageKeys(messageKeys: string[]) {
return React.useCallback(
() => {
return load(messageKeys)
},
messageKeys
)
}
Because of current React implementation, load does not get called when messageKeys change from [] to ['a'].
Update (how we currently solved this)
function areArraysEqual<T>(prevDeps: T[], nextDeps: T[]): boolean {
if (prevDeps === nextDeps) {
return true
}
if (prevDeps.length !== nextDeps.length) {
return false
}
for (let i = 0; i < prevDeps.length; i++) {
if (!Object.is(nextDeps[i], prevDeps[i])) {
return false
}
}
return true
}
export function useLoadMessageKeys(messageKeys: string[]) {
const messageKeysRef = React.useRef(messageKeys)
if (!areArraysEqual(messageKeys, messageKeysRef.current)) {
messageKeysRef.current = messageKeys
}
const currentMessageKeys = messageKeysRef.current
return React.useCallback(
() => load(currentMessageKeys),
[currentMessageKeys]
)
}
If the code linked in the question properly compared 2 arrays we'd avoid having this complexity.
I think the reasoning is that the array of dependencies should always be exactly the list of variables used inside the effect. So in particular it can be statically determined and cannot change size. If it does change size, you're probably doing something more than just listing the dependencies, so it is warning you that you are not using the dependency list as intended.
You could use instead use a version of messageKeys that does not change if it is only shallow equal to the previous one (untested):
const useMemoizedArray = (array: string[]) => {
const [memoizedArray, setMemoizedArray] = React.useState(array);
React.useEffect(() => {
// Define `isShallowEqual` yourself somewhere
if (!isShallowEqual(array, memoizedArray)) {
setMemoizedArray(array);
}
}, [array, memoizedArray]);
return memoizedArray;
};
function useLoadMessageKeys(messageKeys: string[]) {
const memoizedMessageKeys = useMemoizedArray(messageKeys);
return React.useCallback(
() => {
return load(memoizedMessageKeys)
},
[memoizedMessageKeys]
)
}

How is useState() updating my data here? STRANGE

I have data from an movie-api I want to sort based on a select menu, either by year in descending order or title in alphabetical order.
Although Im only updating state in the sort function, not using the variable any where, the data thats already mapped out, and in a different array, updates accordingly. I guess its somehow related to the first change in state for each of the two differen variables?
Any idea how I should solve this correctly and why this is happening?
const sortData = (e) => {
if (e === "year"){
const yearData = data.sort(function(a, b) {
const yearA = a.Year;
const yearB = b.Year;
if (yearA < yearB) {
return -1;
}
if (yearA > yearB) {
return 1;
}
return 0;
});
setYearData(yearData);
}
if (e === "title") {
const titleData = data.sort(function(a, b) {
const titleA = a.Title.toUpperCase();
const titleB = b.Title.toUpperCase();
if (titleA < titleB) {
return -1;
}
if (titleA > titleB) {
return 1;
}
return 0;
});
setTitleData(titleData);
}
}
The sort() method sorts the elements of an array in place, so the data(state) changed without using setState (It may cause some unpredictability happened in the execution)
You can use the sort() method on a copy of the array, so it doesn't affect your state array, I guess this change may work:
use [...data].sort(...) instead of data.sort(...)
Array.sort(), in your case data.sort() updates the original array in addition to returning it. Seems like your data variable is some sort of global that gets changed during sort().

ReactJS. Function declared in a loop contains unsafe references to variable(s) [duplicate]

Building a Sort-Visualizer in React using the Create-React-App [https://roy-05.github.io/sort-visualizer/ ]
I'm animating each iteration of the loop using setTimeouts. On dev console I get the following warning:
Line 156:32: Function declared in a loop contains unsafe references to variable(s) 'minimum', 'minimum', 'minimum', 'minimum' no-loop-func
Here's the code-snippet:
for(let i=0; i<arr.length-1; i++){
let minimum = i; //Declare minimum here
setTimeout(()=>{
for(let j = i+1; j<arr.length; j++){
setTimeout(()=>{
//Getting a warning for these references:
array_bar[j].style.backgroundColor = 'red';
array_bar[minimum].style.backgroundColor = 'blue';
setTimeout(()=>{
if(arr[j] < arr[minimum]){
array_bar[minimum].style.backgroundColor = 'lightblue';
minimum = j;
}
else{
array_bar[j].style.backgroundColor = 'lightblue';
}
}, 4);
}, (j-1)*4);
}
Going through ESLint Docs, I believe the issue might be that i'm modifying the value inside the setTimeout but the variable is declared outside its scope.
I'm not sure how to fix that warning, any help will be appreciated!
Note: Here's the entire function if you need it -
selectionSort(){
const arr = this.state.array,
array_bar = document.getElementsByClassName("array-elem");
this.setState({startedSelectionSort: true});
for(let i=0; i<arr.length-1; i++){
let minimum = i; //Declare minimum here
setTimeout(()=>{
for(let j = i+1; j<arr.length; j++){
setTimeout(()=>{
//Getting a warning for these references:
array_bar[j].style.backgroundColor = 'red';
array_bar[minimum].style.backgroundColor = 'blue';
setTimeout(()=>{
if(arr[j] < arr[minimum]){
array_bar[minimum].style.backgroundColor = 'lightblue';
minimum = j;
}
else{
array_bar[j].style.backgroundColor = 'lightblue';
}
}, 4);
}, (j-1)*4);
}
setTimeout(()=>{
let temp = arr[i],
arr1_height = arr[minimum],
arr2_height = arr[i];
arr[i] = arr[minimum];
arr[minimum] = temp;
array_bar[i].style.height = `${arr1_height}px`;
array_bar[minimum].style.height = `${arr2_height}px`;
array_bar[i].style.backgroundColor = "green";
if(i !== minimum){
array_bar[minimum].style.backgroundColor = 'lightblue';
}
}, 400);
if(i === arr.length-2){
setTimeout(()=>{
array_bar[i+1].style.backgroundColor = "green";
},800);
}
}, i*400);
}
setTimeout(()=>{
this.setState({sorted: true})
}, arr.length*400+1750);
}
I also encountered same warning. In my case, I declared variable outside the iteration, but modified variable inside forEach method.
Something like:
// some code above
let validInputs = true;
someInputs.forEach( input => {
validInputs = input.value && validInputs;
})
After I did some reserch, I found in this post, JSHint error : Functions declared within loops referencing an outer scoped variable may lead to confusing semantics, mentioned that JSHint doesn't like how the anonymous function in there is being re-created over and over.
I changed forEach arrow function to for (let index i = 0; index < someInputs.length; index++), and the warning is gone.
Perhaps in your case, change setTimeout to traditional non-arrow function can remove the warning.
updated on Apr 7th 2021
As I'm reading the Professional JavaScript for Web Developers, 4th edition, I might have found why this warning is implemented in the ESLint.
From section 4.3 Garbage Collection sections, the book mentioned that closure might also lead to memory leak.
The purpose for forEach and arrow function is to limit the scope of the variable, as describes below from MDN:
Arrow functions establish "this" based on the scope the Arrow function is defined within. from Arrow function expressions
In section Creating closures in loops: A common mistake, MDN mentioned:
Another alternative could be to use forEach() to iterate over the helpText array and attach a listener to each , as shown:
function showHelp(help) {
document.getElementById('help').textContent = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
helpText.forEach(function(text) {
document.getElementById(text.id).onfocus = function() {
showHelp(text.help);
}
});
}
setupHelp();
In our implementation, calling arrow functions inside forEach is creating closure of closure, which obviously can create some confusing semantics for garbage collection.
You're correct that modifying the variable inside setTimeout is causing the issue. You can get around this by wrapping setTimeout inside a promise and waiting for it to resolve before modifying your variables. This is much cleaner using async/await:
for (let i = 0; i < arr.length - 1; i++) {
let minimum = i;
await new Promise(resolve => setTimeout(resolve, i * 400));
for (let j = i + 1; j < arr.length; j++) {
array_bar[j].style.backgroundColor = "red";
array_bar[minimum].style.backgroundColor = "blue";
await new Promise(resolve => setTimeout(resolve, (j - 1) * 400));
if (arr[j] < arr[minimum]) {
array_bar[minimum].style.backgroundColor = "lightblue";
minimum = j;
}
}
}
With each loop, you're creating a promise that resolves once the timeout is expired. Using await will pause execution of your function until the promise resolves. Then, you can modify variables like minimum because they are no longer within the scope of the callback function you were originally passing into setTimeout.
Using typescript and React, I was able to initialize minimum inside of the for loop call, and then reinitialize once I got inside:
for (let i, minimum = 0; i < arr.length - 1; i++) {
minimum = i; //reinitialize minimum here
setTimeout(() => {
for (let j = i + 1; j < arr.length; j++) {
setTimeout(() => {
//Getting a warning for these references:
array_bar[j].style.backgroundColor = "red";
array_bar[minimum].style.backgroundColor = "blue";
setTimeout(() => {
if (arr[j] < arr[minimum]) {
array_bar[minimum].style.backgroundColor = "lightblue";
minimum = j;
} else {
array_bar[j].style.backgroundColor = "lightblue";
}
}, 4);
}, (j - 1) * 4);
}
});
}
For me redeclaring the variables in the timeout function did remove that warning for me in FirebaseFunctions.
setTimeout(async ()=> {
var NumberInst = await admin
.firestore()
.collection("CollName")
.doc('DocName')
.get();
var Numbers = NumberInst.data().postponeX;
}, 1000 * 60 * 11 );

Vue: Array is only updated on console, not on screen

I'm still learning the basics of vue. I have different elements in one component, that have to be assigned in a second component. When I use console.log the array is shown correctly, but when i want to show the array in dynamically in template it is still the default value. What am I doing wrong?
Component one:
<template>
<div>
{{nr}}
{{containertyp}}
<button #click="click(0)"></button>
</div>
</template>
I have many more buttons with different parameters, just to make it shorter here.
export default: {
data: function {
return {
nr: [],
containertyp: [],
}
}
methods: {
click(number) {
for (var i = 0; i < 27; i++) {
this.nr[i] = false;
if (number == i) {
this.nr[i] = true;
}
};
EventBus.$emit('containerclicked');
}
},
created: function() {
let i;
//default
for (i = 0; i < 27; i++) {
this.nr[i] = false;
}
for (var index = 0; index < 27; index++) {
this.containertyp[index] = 'bs';
}
},
mounted() {
const self = this
EventBus.$on('containertypchosen', function (containertyp) {
for (let j = 0; j < 27; j++) {
if (self.nr[j] == true) {
self.containertyp[j] = containertyp
}
}
})
Component two:
<template>
<div>
<button :disabled = "disabled == true" v-on:click="chosetype('bs')" "> bs </button> <br />
</div>
</template>
export default: {
data: function() {
return {
disabled: true
}
}
mounted () {
const self = this
EventBus.$on('containerclicked', function (){
self.disabled = false
})
},
methods: {
chosetype: function (containertyp) {
this.containertyp = containertyp
EventBus.$emit('containertypchosen', containertyp)
}
}
}
You can't update arrays using indexes, the changes won't be detected by the reactivity system.
https://v2.vuejs.org/v2/guide/list.html#Caveats
So, for example, this won't work:
this.nr[i] = true;
Instead you'd need to use Vue.set, or the alias vm.$set:
this.$set(this.nr, i, true);
An alternative would be to create a new array and then replace this.nr entirely, i.e. this.nr = newArray.
You'll need to make a similar change everywhere that you're updating an array by index. There are updates to both nr and containertyp that currently have this problem.
It's not immediately clear from your code whether nr even needs to be an array. It seems that all the values are false apart from one, so you might be better off just holding the index of the true value instead of using an array.

Return promise inside the for loop

I have a logic like below,
getSpecificCell: function(tableObject, rowText, columnCss) {
var ele = element.all(by.repeater(tableObject)).count().then(function(count) {
for (var i = 0; i < count; i++) {
return element(by.repeater(tableObject).row(i)).getText().then(function(txt) {
if (txt.indexOf(rowText) !== -1) {
return element(by.repeater(tableObject).row(i)).element(by.css('[' + columnCss + ']'));
}
});
}
});
return ele;
}
But it is returning the value in first iteration itself.
Is that possible to return the promise inside this kind of for loop or do we have any other solution for this?
First, you don't need to use for loops with an ElementArrayFinder. That's what the each() method is for.
Second, you shouldn't need to loop at all. It sounds like you should be using filter() to get the table cells that match your specification, though I'm not sure what exactly you're trying to accomplish.
var table = element.all(by.repeater(tableObject));
// list is an ElementArrayFinder of all elements that matched the filter
var list = table.filter(function (elem) {
return elem.getText().then(function (text) {
return txt.indexOf(rowText) !== -1
})
});
// do something with list
list.count().then(function (count) {
console.log(count);
});

Resources