React 'Cannot set properties to undefined': expensive computation? - javascript

I have an issue while rendering the following.
I have a useBoard hook, which should create a new board: a 2d Array, which every cell is an object, in which some of these object have a value between 1-3 included, and the other 'empty cells' have the 0 value.
Cell object:
{
value: 0,
className: ''
}
My useBoard simply call the function to create the board.
export const useBoard = ({ rows, columns, pieces }: IBoardItems) => {
const [board, setBoard] = useState<ICell[][]>();
useEffect(() => {
const getBoard = () => {
const newBoard = createBoard({ rows, columns, pieces });
setBoard(newBoard);
}
getBoard();
}, [])
return [board] as const;
};
And here is the utility function to build it.
export const createBoard = ({ rows, columns, pieces }: IBoardItems) => {
let randomIndexes: number[][] = [];
while (randomIndexes.length < pieces) {
let randomIndex = [getRandomNumber(5, rows), getRandomNumber(0, columns)];
if (randomIndexes.includes(randomIndex)) {
continue;
} else {
randomIndexes.push(randomIndex);
};
};
let board: ICell[][] = [];
for (let i = 0; i < rows; i++) {
board.push(Array(columns).fill(defaultCell));
};
randomIndexes.forEach(([y, x]) => {
board[y][x] = {
value: getRandomNumber(1, 3),
className: ''
}
});
return board;
};
The fact is, that when I click start in my related component, which should render the <Game /> containing the <Board />, sometimes it works, sometimes not, and the console logs the error 'Cannot set properties to undefined'. I think I understand but I'm not sure: is it because could happen, that in some steps, some data is not ready to work with other data?
So, in this such of cases, which would be the best approach? Callbacks? Promises? Async/await?
NB. Before, I splitted the createBoard function into smaller functions, each one with its owns work, but to understand the issue I tried to make only one big, but actually I would like to re-split it.
EDIT: I maybe found the issue. In the createBoard I used getRandomNumber which actualy goes over the array length. Now the problem no longer occurs, but anyway, my question are not answered.

Related

useEffect received a final argument during this render

I am getting this warning:
Warning: useEffect received a final argument during this render, but not during the previous render. Even though the final argument is optional, its type cannot change between renders.
warning screenshot here
//importing ...
const Relevance = () => {
const { data, setData } = useContext(defaultData);
if (data.length > 0) {
const Relevance = data.map((d) => {
return d.relevance;
});
const maxValue = Math.max(...Relevance);
let HashRelevance = new Array(maxValue + 1).fill(0);
for (let i = 0; i < Relevance.length; i++) {
if (Relevance[i] !== null) {
HashRelevance[Relevance[i]]++;
}
}
var keyRelevance = [];
var valueRelevance = [];
for (let i = 0; i < HashRelevance.length; i++) {
if (HashRelevance[i] !== 0) {
keyRelevance.push(i);
valueRelevance.push(HashRelevance[i]);
}
}
}
const [keyRelevanceState, setKeyIntenstityState] = useState([]);
const [valueRelevanceState, setValueIntenstityState] = useState([]);
useEffect(() => {
setKeyIntenstityState(keyRelevance);
}, keyRelevance);
useEffect(() => {
setValueIntenstityState(valueRelevance);
}, valueRelevance);
if (valueRelevanceState !== undefined && valueRelevanceState.length > 0) {
return (
returning component...
)
};
export default Relevance;
What is the error about?
That error is telling you that you're not passing a consistent value to useEffect as its final (second) argument (the optional dependency array) between renders. The dependency array argument you pass useEffect must either always be an array of dependencies (with the same length), or always be undefined (because you left it off, usually), on every render of your component.
You have this code:
if (/*...a condition...*/) {
// ...
var keyRelevance = [];
// ...
}
// ...
useEffect(() => {
// ...
}, keyRelevance);
That's functionally identical to:
var keyRelevance; // ***
if (/*...a condition...*/) {
// ...
keyRelevance = []; // ***
// ...
}
// ...
useEffect(() => {
// ...
}, keyRelevance);
That means you're declaring a keyRelevance variable that will initially have the value undefined, and then only assigning an array to it if a condition is met. Then you're using keyRelevance as the dependencies array.
The second argument to useEffect should be an array of dependencies, not just a single dependency, so wrap keyRelevance in an array:
useEffect(() => {
setKeyIntenstityState(keyRelevance);
}, [keyRelevance]);
// ^ ^
More in the documentation.
But separately, since you're creating a new array every time, that would cause the effect to change every time. Instead, memoize the creation of keyRelevance (and similar) using useMemo or useRef (since useMemo's documentation says it's just for performance optimization, and in your case you need to memoize the value for correctness, not just optimization).
For instance:
const relevanceInfo = useRef(null);
// (Probably put this condition in a function)
if (!relevanceInfo ||
data.length !== relevanceInfo.sourceData.length ||
data.some((d, index) => d.relevance !== relevanceInfo.sourceData[index].relevance)) {
relevanceInfo.keyRelevance = /* ...build the array... */;
relevanceInfo.sourceData = data;
}
// ...
useEffect(() => {
// ...
}, [relevanceInfo.keyRelevance]);
...or similar.
Side note: I wouldn't declare the variable inside the if block and then use it outside the if block. Instead, declare it in the top scope where it'll be used (in this case function scope). (Also, I suggest using let and const, not var; var is effectively deprecated and shouldn't be used in new code.)
Final value, (second parameter) of the useEffect should be in array of different values or empty array. You are providing a valueRelevance as final value of useEffect which itself an array but not in array , like below.
useEffect(() => {
setKeyIntenstityState(keyRelevance);
}, [valueRelevance]);
Or,
useEffect(() => {
setKeyIntenstityState(keyRelevance);
}, [param1, param2]);

store function names in an array and call them one by one using array.forEach()

I am building a react-native has which has almost 15 states to store some data. Now I want is store all these values (and fetch all of them) in/from internal storage, I'm using react-native-mmkv.
//functions from context api
const {setUsername, setToken, setProgress} = useAppContext();
const getsettingsconfig = () => {
const keys = storage.getAllKeys();
console.log(keys); // ['setProgress', 'setUsername', 'setToken']
keys.forEach(key => {
const data = storage.getString(key);
// I want to set all the values dynamically here
key(parsedData); // setProgress(data), setUsername(data) etc. like this
});
};
useAppContext.js
import { createContext, useContext } from "react";
export const GlobalContext = createContext({});
export default function useAppContext() {
return useContext(GlobalContext);
}
However, I keep getting
[TypeError: 'setProgress' is not a function]
What am I doing wrong here?
This is one possible way to execute a function whose name is present in an array. The trick here is mapping the name of the function with the function itself (by using the object myObj). Then, one simply accesses the object with the key, and executes the function (which is the corresponding value in myObj object).
// declaring, defining 4 functions here
// this may even be from useState - same will work
const setField1 = d => console.log('setField1, data: ', d);
const setField2 = d => console.log('setField2, data: ', d);
const setField3 = d => console.log('setField3, data: ', d);
const setField4 = d => console.log('setField4, data: ', d);
// declaring an array of functions
// this can be populated from local-storage - and it will work
const fnArr = [setField1, setField2, setField3, setField4];
// for-each loop to execute each function in the array
fnArr.forEach((fn, idx) => fn(idx));
// using a string array - which will not execute the functions
console.log("\n\n can't execute strings as functions\n\n");
const strArr = ['setField1', 'setField2', 'setField3', 'setField4'];
strArr.forEach(st => console.log(`'${st}' is a string & not a function... one can't execute it`));
// executing the same functions by using the string keys
console.log('\n\n executing functions by using string-names as keys\n\n');
const myObj = Object.fromEntries(strArr.map((k, idx) => ([ k, fnArr[idx]])));
strArr.forEach((k, idx) => myObj?.[k](idx));
.as-console-wrapper { max-height: 100% !important; top: 0 }

Recursive function to return overall overall results

I need a recursive method to roll up all results from a series of paginated calls and returns the complete list of results. Something feels off in the way I am doing it and feel there is a better way to do this, possibly with Array.reduce
Any recommendations appreciated.
interface Result {
users: Widget[];
start: number;
}
interface Widget {
id: number;
}
// create 3 widgets for test
const widgets = Array(3).fill(null).map((i, index: number) => {
return {
id: index + 1,
} as Widget;
});
const getFromAPI = (start: number = 0): Result => {
// return 1 at a time from a specified position
const current = widgets.slice(start, start + 1);
let nextStart: number | undefined;
if (start < widgets.length - 1) {
nextStart = start + 1;
}
return {
users: current,
start: nextStart,
}
}
// I don't like that em is outside the scope here
let em: Widget[] = [];
const getWidgets = (start?: number): Widget[] => {
const result = getFromAPI(start);
em = [...em, ...result.users];
if (result.start) {
getWidgets(result.start);
}
return em;
}
const all = getWidgets();
You don't like that em is outside the scope of getWidget() and that you are reassigning it inside the function body and thus relying on side effects. Instead it seems that you want something more purely functional, without relying on state changes.
If so, then one approach you can take is to make em another argument to the function, and have it initially empty, and then returning the recursive result of getWidgets() called with the next version of em:
const getWidgets = (em: Widget[] = [], start?: number): Widget[] => {
const result = getFromAPI(start);
const nextEm = [...em, ...result.users];
return result.start ? getWidgets(nextEm, result.start) : nextEm;
}
I've changed the reassignment of em to a new variable nextEm just in case you want to avoid side effects even within the body of getWidgets as well. It makes the algorithm a little clearer anyway.
You can verify that a call to getWidgets() yields the same result as in your example:
const all = getWidgets();
console.log(all); // [{ "id": 1}, { "id": 2}, { "id": 3}]
Playground link to code

Function doesn't wait react hooks

I'm trying to add items in shopping cart. It works, but after adding items when I want to calculate number of items to show on the shopping cart. Second function (calculate()) doesn't wait items hooks. Because of that, it shows the correct count after adding second item.
Below code is my functions. As you can see, in the end of first function I'm calling the calculate() function to keep it continue.
const [testArray, setTestArray] = useState([]);
const [total, setTotal] = useState(0);
const [cartCount, setCartCount] = useState(0);
function addToTest(product, quantity = 1) {
const ProductExist = testArray.find((item) => item.id === product.id);
if (ProductExist) {
setTestArray(
testArray.map((item) => {
if (item.id === product.id) {
return { ...ProductExist, quantity: ProductExist.quantity + 1 };
} else {
return item;
}
})
);
} else {
product.quantity = 1;
setTestArray([...testArray, product]);
}
calculate();
}
function calculate() {
let resultCount = 0;
testArray.map((item) => {
console.log("map func works!");
setCartCount(cartCount + item.quantity);
});
}
Here is my codesandbox project, I kept it very simple to not bother you.
https://codesandbox.io/s/react-template-forked-u95qt?file=/src/App.js
The possible problem occurs due to synchronise functions. Because of that, when I try to async/await, I'm getting error about parameters, because the first function has parameter.
This is my first try for async/await:
async function calculate(product) {
await addToTest(product);
let resultCount = 0;
testArray.map((item) => {
console.log("map func works!");
setCartCount(cartCount + item.quantity);
});
}
As other solution I tried to use useEffect by taking the reference setArray hooks. However, in this case, the count number increases exponentially like 1,3,9...
useEffect(()=>{
let resultCount = 0;
testArray.map((item) => {
console.log("map func works!");
setCartCount(cartCount + item.quantity);
});
},[testArray])
I wonder where is the problem? Because when I use the the upper code in Angular/Typescript, it works properly. I think this happens due to react hooks, but I couldn't understand the problem.
Your thought to use a useEffect is a good one, but instead of calling setCartCount on every iteration you should instead sum all the item.quantity counts first and then call setCartCount once after your loop.
Note: don't use map() if you're going to ignore the array it returns, use a for loop, for...of as in the example below, or forEach() instead.
useEffect(() => {
let resultCount = 0;
for (const item of testArray) {
resultCount += item.quantity;
}
setCartCount(resultCount);
}, [testArray]);
or with reduce() (here destructuring and renaming the quantity property of each passed item)
useEffect(() => {
const resultCount = testArray.reduce((a, { quantity: q }) => a + q, 0);
setCartCount(resultCount);
}, [testArray]);
Every call to setCartCount will only update cartCount on the next render, so you are (effectively) only calling it with the final item in the array. You should instead use reduce on the array to get the value you want, and then set it.
See https://codesandbox.io/s/react-template-forked-u95qt?file=/src/App.js

How to update state using settimeout function inside loop in react?

I'm creating react app to visualize sorting arhorithms and I stopped on this problem. I'm looping through all elements of array from bars state and I want to swap them (for test purposes). This is not working excatly as I wanted, because it "ignores" setTimeout function and does it immediately. I tried something with setBars but it is not working too. How can I make this so swapping will happen after timeout set in setTimeout function?
const [bars, setBars] = useState([23, 63, 236, 17, 2]);
const swap = (arr, i, j) => {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
};
for (let i = 0; i < bars.length - 1; i++) {
setTimeout(() => {
swap(bars, i, i + 1);
}, 2000);
}
You'd want to use a useEffect hook to start the process when the component mounts:
useEffect(() => {
// ...code here...
}, []);
The empty dependencies array at the end says "only run this when the component first mounts."
Within it, your code would schedule the timer callbacks:
for (let i = 0; i < bars.length - 1; i++) {
setTimeout(() => {
setBars(bars => {
// Copy the current array
const newBars = [...bars];
// Do the swap
swap(newBars, i, i + 1);
// Set the state by returning the update
return newBars;
});
}, 2000 * (i + 1));
}
Note that that uses the callback form of setBars, so that you're operating on the then-current state (instead of bars, which is only the array used during the first mount, since your function is only called when that first mount occurs).
Also note that the interval is 2000 * (i + 1) rather than just 2000. That's so each callback occurs 2000ms after the last one, since they're all being scheduled at once.
Another important aspect of the above is that it uses let within the for, so each loop body gets its own i variable. (If the code used var, or let outside the for, all the callbacks would share the same i variable, which would have the value bars.length.)
Alternatively, I think I might take this approach:
Since you want to update both i and bars at the same time and since it's best to use the callback form of the state setter when doing updates asynchronously (as with setTimeout), I'd combine i and bars into a single state item (whereas I'd normally keep them separate):
const [barState, setBarState] = useState({i: 0, bars: [23, 63, 236, 17, 2]});
Then you'd use a useEffect hook to kick the process off when your component mounts and then do it again after each change to barState:
useEffect(() => {
// ...code here...
}, [barState]);
The dependencies array containing barState at the end says "run this on component mount and every time barState changes."
Within it, your code would schedule the timer callbacks:
if (barState.i < barState.bars.length - 1) {
setTimeout(() => {
setBarState(({i, bars}) => {
// Copy the current array
bars = [...bars];
// Do the swap
swap(bars, i, i + 1);
// Set the state by returning the update
++i;
return {i, bars};
});
}, 2000);
}
Live Example:
const { useState, useEffect } = React;
const swap = (arr, i, j) => {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
};
function Example() {
const [barState, setBarState] = useState({i: 0, bars: [23, 63, 236, 17, 2]});
useEffect(() => {
if (barState.i < barState.bars.length - 1) {
setTimeout(() => {
setBarState(({i, bars}) => {
// Copy the current array
bars = [...bars];
// Do the swap
swap(bars, i, i + 1);
// Set the state by returning the update
++i;
return {i, bars};
});
}, 2000);
}
}, [barState]);
return <div>{barState.bars.join(", ")}</div>;
}
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>

Categories

Resources