Function doesn't wait react hooks - javascript

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

Related

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

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.

Delete All Array Elements With Animation

I have a react native todo list app.
The task items are represented by an array using useState which is declared as follows:
const [taskItems, setTaskItems] = useState([]);
I'm trying to add a function that deletes all of the items but instead of just setting it like this setTaskItems([]), which simply deletes all items at once, I was hoping to delete the items one at a time starting from the end to create a more animated look.
while(taskItems.length > 0)
{
alert(taskItems.length);
let itemsCopy = [...taskItems];
itemsCopy.splice(-1);
setTaskItems(itemsCopy);
//setTimeout(function(){},180);
}
For some reason the above code causes infinite recursion. If I remove the while loop then the deleteAll function does indeed remove the last item in the list. Based on my placing the alert at the beginning of the function it seems like the length of the array is never decreased.
Why is that?
And is there a better way to achieve this effect?
It happen because in your code taskItem value doesn't change when you use setTaskItems
setTaskItems is an asynchronous function and the updated value of the state property change after the rerender of the component
you can try something like this
const [taskItems, setTaskItems] = useState([]);
const [deleteAll, setDeleteAll] = useState(false)
useEffect(() => {
if(!deleteAll) return;
if(taskItems.lenght === 0){
setDeleteAll(false)
return;
}
setTimeout(() => {
setTaskItems(t => t.slice(0, -1))
}, 180)
}, [deleteAll, taskItems])
const handleDeleteAll = () => {
seDeleteAll(true)
}
That is because taskItems value is retrieved from closure, meaning you can not expect value to be changed during handler execution. Closure recreates only after element rerenders, meaning you can access new state value only in next function execution - during the function execution value of the state never change.
You just need to assign length value to some temp variable, and use it as start/stop indicator for while loop:
let itemsCount = taskItems.length;
while(itemsCount > 0)
{
// Here use itemsCount to determine how much you would need to slice taskItems array
itemsCount -= 1;
}
UPDATED
An idea of how you should do it is:
const [items, setItems] = useState([1, 2, 3, 4, 5]);
const deleteAll = () => {
let counter = items.length;
while (counter > 0) {
setTimeout(() => {
setItems((p) => [...p.slice(0, counter - 1)]);
}, counter * 1000);
counter -= 1;
}
};
It is important to multiply timeout delay with length, in order to avoid simultaneous execution of all timeouts - you need one by one to be deleted.
Also you should clear timeout on unmount or something like that.
Due to asynchronous nature of React useState hook, you need to harness useEffect to do the job. Here's working example of removing all tasks with a 1000 ms delay between items.
function TasksGrid() {
const [taskItems, setTaskItems] = useState([1, 2, 3, 4, 5, 6]);
const [runDeleteAll, setRunDeleteAll] = useState(false);
useEffect(
() => {
if (runDeleteAll) {
if (taskItems.length > 0)
setTimeout(() => setTaskItems(taskItems.slice(0, taskItems.length - 1)), 1000);
else
setRunDeleteAll(false);
}
},
[runDeleteAll, taskItems]
)
const handleOnClick = () => {
setRunDeleteAll(true);
}
return (
<div>
{taskItems.map((taskItem, idx) => <p key={idx}>task {taskItem}</p>)}
<button onClick={handleOnClick}>delete all</button>
</div>
);
}

how to use variable immediately after it's defined - js

How, sorry for this weird title, i didn't know how to put this... Here my explanation:
I have a function where I define variable (I'm looping over an array and if condition is matched, I define let). It works everytime item.dataset.category changes, so after every scroll.
After the loop, I call another function, where I use this variable as an argument. In the second function I use it to check if another condition is matched:
//first function
const getDataset = (e) => {
let dataset;
const cat = Array.from(categories.children);
cat.forEach((item) => {
if (item.className.includes('active')) {
dataset = item.dataset.category;
}
});
changeNavActive(dataset);
};
//second function
const changeNavActive = (dataset) => {
const navItems = Array.from(navList.children);
navItems.forEach((item) => {
item.classList.remove('active');
if (item.dataset.category === dataset) {
item.classList.add('active');
}
});
};
It's not working and I think I understand why - callign of second function is at the same time as declaring variable, so I geting this let in the next call. The result is that second function works with delay of one scroll.
This is a function which calls getDataset():
const scrollRows = (e) => {
if (window.scrollY > slider.clientHeight) {
e.deltaY > 0 ? move++ : move--;
getDataset();
if (move > categories.children.length - 1)
move = categories.children.length - 1;
}
}
How to fix this?

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>

Implement unregister after registration of callback

I wrote a simple piece of software that allows users to "register" a function when a state is set.
This was easily achieved by adding functions to an array.
I want to return a function that is able to "unregister" that particular function.
Note that a user might register the same function twice. This means that the "unregistering" function cannot be based on the function as a key in a map
The only thing that springs to mind is making the "register" function way more complex, where each item in the "callbacks" array is not just a function, but an object like this:
{
id: someId
fn: [the function]
}
And that the unregister function will filter the someId value. But I just can't like this.
Ideas?
const state = {}
const callbacks = []
const register = (fn) => {
callbacks.push(fn)
return () => {
console.log('Unregister function. HELP!!! How do I do this?')
}
}
const setState = async (key, value) => {
state[key] = value
for (const fn of callbacks) fn(key, value)
}
const getState = (key) => {
return state[key]
}
const f1 = () => {
console.log('f1')
}
const f2 = () => {
console.log('f2')
}
const unregF1a = register(f1)
const unrefF1b = register(f1)
const unregF2 = register(f2)
setState('some', 'a')
unregF1a()
setState('some', 'b')
Loop through your callbacks and remove the desired function (works if the same function is registered twice).
You could do a simple for loop:
function unregister(fn) {
for (let i = callbacks.length - 1; i >= 0; i--) {
if (callbacks[i] === fn) {
callbacks.splice(i, 1)
}
}
}
Or you can use let and replace the whole array:
let callbacks = [];
function unregister(fn) {
callbacks = callbacks.filter(cb => cb !== fn)
}
If you want to be able to register the same function more than once and be able to unregister them independently, then yes, you'll need to track some kind of id.
An id can be something simple, like an increasing integer, and you can store them in a different array, in the same index the function is in the callbacks array (that's hashing).
Something like this:
const state = {}
const callbacks = []
const ids = []
let nextId = 0
const register = (fn) => {
const id = nextId
callbacks.push(fn)
ids.push(nextId)
nextId++
return () => {
// find the function position using the ids array:
const fnIndex = ids.findIndex(cbId => cbId === id)
if (fnIndex === -1) return // or throw something
// Now remove the element from both arrays:
callbacks.splice(fnIndex, 1)
ids.splice(fnIndex, 1)
}
}
This way, the unregister function always looks for the exact index where the id/fn resides.

Categories

Resources