Updating state twice when the button is clicked, React - javascript

I want to create simple 2048 game application. My initial board is composed of 16 array elements.
The generate() function generates a value of '2' in a random empty element
It works just fine and creates one random '2' for me, but the problem starts when I want to call it twice in a handler like this:
const handleNewGame = () => {
generate()
generate()
}
I've read about prevState but have no idea how to implement it in this batch of code to work properly.
Here is my game component:
const width = 4;
const Game = () => {
const [Board, setBoard] = useState([]);
const createBoard = () => {
let initialBoard = [];
for (let i = 0; i < width * width; i++) {
initialBoard.push("");
}
return initialBoard;
};
const generate = () => {
let board = [...Board];
let randomNumber = Math.floor(Math.random() * Board.length);
console.log(randomNumber);
if (board[randomNumber] === "") {
board[randomNumber] = 2;
setBoard(board);
} else generate()
};
const handleNewGame = () => {
generate()
generate()
}
useEffect(() => {
setBoard(createBoard);
console.log(`Board Created!`);
}, []);
return (
<div className="game-container">
<button onClick={handleNewGame}>NewGame</button>
<div className="game">
{Board.map((value, index) => (
<div className="tile" key={index}>
{value}
</div>
))}
</div>
</div>
);
};
export default Game;
I'll be glad for the answer.

setState(), named setBoard() in your code is asynchronous, whatch this great video on the Event Loop for you to understand more: https://www.youtube.com/watch?v=8aGhZQkoFbQ.
See if this will suit your needs:
import { useEffect, useState } from 'react';
const width = 4;
const Game = () => {
const [Board, setBoard] = useState([]);
const createBoard = () => {
let initialBoard = [];
for (let i = 0; i < width * width; i++) {
initialBoard.push('');
}
return initialBoard;
};
const randomBoardPosition = () => {
return Math.floor(Math.random() * Board.length);
};
const generate = () => {
let board = createBoard();
let randomNumber = randomBoardPosition();
let positionsFilled = 0;
while (positionsFilled < 2) {
if (board[randomNumber] === '') {
board[randomNumber] = 2;
positionsFilled++;
} else {
randomNumber = randomBoardPosition();
}
}
setBoard(board);
};
const handleNewGame = () => {
generate();
};
useEffect(() => {
setBoard(createBoard);
console.log(`Board Created!`);
}, []);
return (
<div className="game-container">
<button onClick={handleNewGame}>NewGame</button>
<div className="game">
{Board.map((value, index) => (
<div className="tile" key={index}>
{value}
</div>
))}
</div>
</div>
);
};
export default Game;

Just us function instead of value
const generate = () => {
setBoard(Board => {
let board = [...Board];
while (true) {
let randomNumber = Math.floor(Math.random() * Board.length);
console.log(randomNumber);
if (board[randomNumber] === "") {
board[randomNumber] = 2;
break
}
}
}
}

Related

Looping to clear all numbers colors in react

I have this working function to change all children button color on click.
Now im doing a "ClearGame" button, to change all button background color to its original state '#ADC0C4'. How can i do that with key or id from the children?
const newBet: React.FC = () => {
const clearGame = () => {
let spliceRangeJSON = gamesJson[whichLoteriaIsVar].range;
totalNumbers.splice(0, spliceRangeJSON);
for (let i = 0; i <= spliceRangeJSON; i++) {
//Looping to change the backgroundColor using Id or Key
}
};
const NumbersParent = (props: any) => {
const [numbersColor, setNumbersColor] = useState('#ADC0C4');
const changeButtonColor = () => {
if (numbersColor === '#ADC0C4') {
setNumbersColor(gamesJson[whichLoteriaIsVar].color);
totalNumbers.push(props.id);
} else {
setNumbersColor('#ADC0C4');
let searchTotalNumbers = totalNumbers.indexOf(props.id);
totalNumbers.splice(searchTotalNumbers, 1);
}
};
return (
<Numbers style={{ backgroundColor: numbersColor }} onClick={changeButtonColor}>
{props.children}
</Numbers>
);
};
return (
<NumbersContainer>
{numbersList.map((num) => (
<NumbersParent key={num} id={num}>
{formatNumber(num)}
</NumbersParent>
))}
</NumbersContainer>
<ClearGame onClick{clearGame}>Clear Game</ClearGame >
);
};
Since you want to modify the state of children from the parent component,
Create a State Object in the parent.
Pass it to the children as prop
You can change it on clear.
Or try something like recoil.
Move the useState and changeButtonColor method to the parent component and make some changes:
const [numbersColor, setNumbersColor] = useState({});
const changeButtonColor = (color) => {
if (!numbersColor[color] || numbersColor[color] === '#ADC0C4') {
setNumbersColor({...numbersColors,
[color]: gamesJson[whichLoteriaIsVar].color
});
totalNumbers.push(props.id);
} else {
setNumbersColor({...numbersColors,
[color]: '#ADC0C4'
});
let searchTotalNumbers = totalNumbers.indexOf(props.id);
totalNumbers.splice(searchTotalNumbers, 1);
}
};
Change the map to that:
<NumbersContainer>
{numbersList.map((num, index) => (
<NumbersParent key={num} id={num} index={index} changeButtonColor={(color) => {changeButtonColor(color)}}>
{formatNumber(num)}
</NumbersParent>
))}
</NumbersContainer>
And on the return of NumbersParent component changes to be like that:
<Numbers style={{ backgroundColor: numbersColor[`color${props.index}`] }} onClick={props.changeButtonColor(`color${props.index}`)}>
{props.children}
</Numbers>
Finally, change clearGame function like this:
const clearGame = () => {
let spliceRangeJSON = gamesJson[whichLoteriaIsVar].range;
for (let i = 0; i <= spliceRangeJSON; i++) {
changeButtonColor(`color${i}`)
}
totalNumbers.splice(0, spliceRangeJSON);
};

Setting setTimeOut in a react class base component

Plaese I want to setTimeOut in my class base component. Trying different method but still it did not work. I want the setTimeout to call nextClick/onClickI after a specified second but, still don't understand how to go about it. Please help, if you have an idea. below is the component code...
class AbCarousel extends Component {
state = {
data: []
}
myRef = React.createRef();
getData = async () => {
const res = await fetch('aboutdata.json');
const data = await res.json();
this.setState({data: data})
}
componentDidMount() {
this.getData();
}
prevClick = () => {
const slide = this.myRef.current;
slide.scrollLeft -= slide.offsetWidth;
if (slide.scrollLeft <= -0){
slide.scrollLeft = slide.scrollWidth;
}
};
nextClick = () => {
const slide = this.myRef.current;
slide.scrollLeft += slide.offsetWidth;
if (slide.scrollLeft >= (slide.scrollWidth - slide.offsetWidth)) {
slide.scrollLeft = 0;
}
};
render() {
const { data } = this.state;
return (
<div className="wrapper">
<div className="app" ref={this.myRef}>
<AbCard data={data} />
</div>
<div className="row">
<div className="prev" onClick={this.prevClick}>
<div><FaChevronLeft /></div>
</div>
<div className="next" onClick={this.nextClick}>
<div><FaChevronRight /></div>
</div>
</div>
</div>
)
}
}
export default AbCarousel
Simplest is just to update your onClick handler to take setTimeout as a callback like the following
onClick={() => setTimeout(this.nextClick, numSeconds)
or you can update the nextClick method directly and use a callback function within the setTimeout and wrap around the code you wish to delay the execution on:
nextClick = () => {
const slide = this.myRef.current;
setTimeout(() => {
slide.scrollLeft += slide.offsetWidth;
if (slide.scrollLeft >= (slide.scrollWidth - slide.offsetWidth)) {
slide.scrollLeft = 0;
}, numSeconds)
}

How to keep the current state of a page after a refresh? - React

I am using a custom pagination component that paginates the data from an array. When a user goes to a certain page and refreshes, it takes the user back to the first page. I am thinking I can use Local Storage to handle this. See below:
export interface PublicProps {
ID: number;
}
const PAGE_SIZE: number = 10;
export const TestPage: React.FC<PublicProps> = ({ ID }) => {
const [currentPage, setCurrentPage] = useState<number>(1);
const suitesForProject = SOME DATA FROM AN ARRAY
const totalPages = suitesForProject.length
? Math.ceil(suitesForProject.length / PAGE_SIZE)
: 0;
const handleClickNext = () => {
if (!suitesForProject.length) {
return;
}
setCurrentPage((currentPage) => Math.min(currentPage + 1));
};
const handleClickPrev = () => {
if (!suitesForProject.length || currentPage === 1) {
return;
}
setCurrentPage((currentPage) => currentPage - 1);
};
return (
<>
{suitesForProject
.slice((currentPage - 1) * PAGE_SIZE, PAGE_SIZE * currentPage)
.map((suitesForProject) => (
//doing stuff with the data here
))}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onClickPrevious={handleClickPrev}
onClickNext={handleClickNext}
previousPageButtonAriaLabel="To previous page"
nextPageButtonAriaLabel="To next page"
/>
</>
);
};
Is there a way to handle this?
useState has lazy loading. It is looks like
useState(() => 1); function as first arg is lazy function which called once. Inside this function read localStorage by key, parse value and if it is number return it, else return 1.
You can do something like this..
export interface PublicProps {
ID: number;
}
const PAGE_SIZE: number = 10;
const PAGE_KEY = "MY_PAGINATION_KEY";
const getPageNumber = () => {
if(localStorage && parseInt(localStorage.getItem(PAGE_KEY)) > 0) {
return parseInt(localStorage.getItem(PAGE_KEY));
}
return 1;
}
export const TestPage: React.FC<PublicProps> = ({ ID }) => {
const [currentPage, setCurrentPage] = useState<number>(getPageNumber());
const suitesForProject = SOME DATA FROM AN ARRAY
const totalPages = suitesForProject.length
? Math.ceil(suitesForProject.length / PAGE_SIZE)
: 0;
const handleClickNext = () => {
if (!suitesForProject.length) {
return;
}
localStorage.setItem(PAGE_KEY, currentPage +1)
setCurrentPage((currentPage) => Math.min(currentPage + 1));
};
const handleClickPrev = () => {
if (!suitesForProject.length || currentPage === 1) {
return;
}
localStorage.setItem(PAGE_KEY, currentPage - 1)
setCurrentPage((currentPage) => currentPage - 1);
};
return (
<>
{suitesForProject
.slice((currentPage - 1) * PAGE_SIZE, PAGE_SIZE * currentPage)
.map((suitesForProject) => (
//doing stuff with the data here
))}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onClickPrevious={handleClickPrev}
onClickNext={handleClickNext}
previousPageButtonAriaLabel="To previous page"
nextPageButtonAriaLabel="To next page"
/>
</>
);
};

React useState to update a specific item in its array

I have an array of images inside refData. I then map them into an array of img objects with RefItem. I then want thouse specifc images to change to the next one in a line - so img 0 becomes 1, and 1 becomes 2 - as written in refbox. I just cannot figure this one out.. I just want to update the individual numbers in each array item?
import React, {useState, useEffect} from 'react'
import refData from './refData'
import RefItem from './refItem'
function ReferencesPP(){
const refItems = refData.map(item => <RefItem key={item.id} pitem={item}/>)
const [refs, setRefs] = useState([0, 1, 2, 3])
useEffect(() => {
const intervalId = setTimeout(() => {
for(let i = 0; i < refs.length; i++){
if(refs[i] !== refData.length){
setRefs(...refs[i], refs[i] = refs[i] + 1)
} else {
setRefs(...refs[i], refs[i] = 0)
}
}
}, 2000);
return () => clearTimeout(intervalId);
}, [refs]);
return(
<div className="referencespp-container">
<div className="background-container" />
<div className="content-container">
<div id="refbox">
{refItems[refs[0]]}
{refItems[refs[1]]}
{refItems[refs[2]]}
{refItems[refs[3]]}
</div>
</div>
</div>
)
}
export default ReferencesPP
I thought it would be as simple, as writing
const refs = [0, 1, 2, 3]
useEffect(() => {
const intervalId = setInterval(() => {
for(let i = 0; i < refs.length; i++){
if(refs[i] !== refData.length){
refs[i] = refs[i] + 1;
} else {
refs[i] = 0;
}
}
}, 2000);
return () => clearInterval(intervalId);
}, []);
but doing that only updates the const and not the {refItems[refs[0]]} elements?
Have a look at this https://jsfiddle.net/gwbo4pdu/ I think it's what you want to get
React.useEffect(() => {
const intervalId = setTimeout(() => {
const newRefs = [...refs]
const i = newRefs.shift()
newRefs.push(i)
setRefs(newRefs);
}, 2000);
return () => clearTimeout(intervalId);
}, [refs]);
P.S. you can do just newRefs.push(newRefs.shift())

backgroundColor doesn't re-render - React

i'm building a sorting algorithm visualizer. And every one of these items is a div with the backgroundColor set to white.
When the algorithm is running he sets the backgroundColor to orange to display which items have changed.
But the problems happens when i reset the array using setState(), because react re-renders the new array perfectly fine, but he never renders the backgroundColor back to white.
This is my component:
import React from 'react'
import './SortingVisualizer.css'
import InsertionSort from '../SortingAlgorithms/InsertionSort'
const NORMAL_COLOR = 'white';
const CHANGED_COLOR = 'red';
const AFTER_CHANGE_COLOR = 'orange';
export default class SortingVisualizer extends React.Component{
constructor(props){
super(props);
this.state = {
arrayToSort: []
};
}
componentDidMount(){
this.resetArray();
}
resetArray(){
const arrayToSort = [];
for (let i = 0; i < 200; i++) {
arrayToSort.push(this.RandomIntBetweenRange(5, 1000));
}
this.setState({ arrayToSort });
}
insertionSort(){
let sortedArrayAnim = InsertionSort(this.state.arrayToSort);
let arrayToSort = this.state.arrayToSort;
let arrayBars = document.getElementsByClassName('array-item');
let arrayBarsWithColorChanged = [];
//loop through all the animations
for (let index = 0; index < sortedArrayAnim.length; index++) {
const [i,j] = sortedArrayAnim[index];
//setTimeout(() => {
//set changed colors back to normal
if(index !== 0){
arrayBarsWithColorChanged.forEach((element, index) => {
arrayBars[element].style.backgroundColor = AFTER_CHANGE_COLOR;
});
arrayBarsWithColorChanged = [];
}
let temp = arrayToSort[i];
//change array
arrayToSort[i] = arrayToSort[j];
arrayToSort[j] = temp;
//change div bar colors, unl
if(index != sortedArrayAnim.length - 1){
arrayBars[i].style.backgroundColor = CHANGED_COLOR;
arrayBars[j].style.backgroundColor = CHANGED_COLOR;
arrayBarsWithColorChanged.push(i);
arrayBarsWithColorChanged.push(j);
}
this.setState({ arrayToSort })
//}, 10);
}
}
render() {
const {arrayToSort} = this.state;
return (
<div className="main-div">
{arrayToSort.map((value, idx) => (
<div className="array-item" key={idx} style={{height: value, backgroundColor: 'white'}}>
</div>
))}
<button onClick={() => this.resetArray()}>Generate new array</button>
<button onClick={() => this.insertionSort()}>Insertion Sort</button>
</ div>
);
}
RandomIntBetweenRange(min, max){
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
In React you don't manipulate DOM directly and you don't mutate the state (arrayToSort[i] = arrayToSort[j] for example). You change the model (state, props), and the view changes accordingly.
So in your case, you need to include in the state, the array of column values (arrayToSort), the array of pairs to swap (sortedArrayAnim), and a Set of previous changed values (prevChanged). Whenever something changes, the view will update according to those state values.
Demo (see comments in code):
const NORMAL_COLOR = 'white';
const CHANGED_COLOR = 'red';
const AFTER_CHANGE_COLOR = 'orange';
/** this function return the color **/
const getColor = (idx, prevChanged, [current]) => {
if(current && current.includes(idx)) return CHANGED_COLOR; // if it's in current changed pair [i, j]
if(prevChanged.has(idx)) return AFTER_CHANGE_COLOR; // if it was changed before
return NORMAL_COLOR;
}
class SortingVisualizer extends React.Component {
state = {
arrayToSort: [],
sortedArrayAnim: [],
prevChanged: new Set()
};
timeout = null;
componentDidMount() {
this.resetArray();
}
resetArray = () => {
clearTimeout(this.timeout);
const arrayToSort = [];
for (let i = 0; i < 50; i++) {
arrayToSort.push(this.RandomIntBetweenRange(1, 100));
}
this.setState({
arrayToSort,
sortedArrayAnim: [],
prevChanged: new Set()
});
}
animate = (sortedArrayAnim) => {
this.setState(
({
prevChanged,
arrayToSort
}) => {
if(!sortedArrayAnim.length) return { sortedArrayAnim };
const [current] = sortedArrayAnim;
const newArrayToSort = [...arrayToSort]; // clone newArrayToSort
/** flip the values according to current change [i, j] **/
newArrayToSort[current[0]] = arrayToSort[current[1]];
newArrayToSort[current[1]] = arrayToSort[current[0]];
return ({
arrayToSort: newArrayToSort,
sortedArrayAnim,
prevChanged: new Set([...prevChanged, ...current]) // add changed items to the Set
});
},
() => { // when state change is done
const { sortedArrayAnim } = this.state;
// if there are more items to change wait and call animate again
if(sortedArrayAnim.length) {
this.timeout = setTimeout(() => this.animate(sortedArrayAnim.slice(1)), 1000);
}
}
);
}
insertionSort = () => {
const sortedArrayAnim = [[1, 5], [10, 15], [20, 13], [17, 48], [20, 13], [45, 17]]; // InsertionSort(this.state.arrayToSort); // I've used a dummy array
this.animate(sortedArrayAnim);
}
render() {
const { arrayToSort, sortedArrayAnim, prevChanged } = this.state;
return (
<div className="main-div">
{arrayToSort.map((value, idx) => (
<div className="array-item" key={idx} style={{
height: `${value}vh`,
backgroundColor: getColor(idx, prevChanged, sortedArrayAnim)
}}>
</div>
))}
<button onClick={this.resetArray}>Generate new array</button>
<button onClick={this.insertionSort}>Insertion Sort</button>
</ div>
);
}
RandomIntBetweenRange(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
ReactDOM.render(
<SortingVisualizer />,
root
);
.main-div {
display: flex;
align-items: flex-end;
justify-content: space-between;
background: black;
height: 100vh;
padding: 1vmax 1vmax 0 ;
box-sizing: border-box;
}
.array-item {
width: 1vw;
transition: height 0.3s;
}
button {
align-self: flex-start;
}
body {
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You're going to want to keep everything in state in order for react to re-render based on any changes to state.
state = {
backgroundColor: 'white'
}
...
// element
<div className="array-item" key={idx} style={{height: value, backgroundColor: this.state.backgroundColor}}>
replace arrayBars[i].style.backgroundColor = 'orange'; with this.setState({ backgroundColor: 'orange' });
and update:
resetArray(){
const arrayToSort = [];
for (let i = 0; i < 200; i++) {
arrayToSort.push(this.RandomIntBetweenRange(5, 1000));
}
this.setState({ arrayToSort, backgroundColor: 'white' }); // reset back to white
}

Categories

Resources