I am trying to use the code from this codepen in my react/date-fns app.
import {useEffect, useRef, useState} from 'react'
import {add, sub, format, parse} from 'date-fns'
const timeRegExp = /([01][0-9]|2[0-3])[0-5][0-9]/
const Time = () => {
const refs = useRef([])
const [values, setValues] = useState([])
useEffect(() => {
refs.current.forEach((input, i) => {
input.addEventListener('keydown', (e: any) => {
if (e.keyCode === 37) {// left arrow
if (i !== 0) refs.current[i - 1].focus()
} else if (e.keyCode === 39) {// right arrow
if (i !== 3) refs.current[i + 1].focus()
} else if (/48|49|50|51|52|53|54|55|56|57/.test(e.keyCode)) {// 0 ~ 9
const time = [0, 1, 2, 3].map(i => {
return i === i ? String.fromCharCode(e.keyCode) : refs.current[i].value
}).join('')
if (timeRegExp.test(time)) {
refs.current[i].value = String.fromCharCode(e.keyCode)
if (i !== 3) refs.current[i + 1].focus()
}
} else if (e.keyCode === 8) {// delete / backspace
refs.current[i].value = 0
if (i !== 0) refs.current[i - 1].focus()
} else if (e.keyCode === 38) {// up arrow
// if (i === 0 && refs.current[0].value === '2') {
//
// } else {
// let time = [0, 1, 2, 3].map(i => refs.current[i].value).join('')
// time = moment(time, 'HHmm').add(i % 2 ? 1 : 10, Math.floor(i / 2) ? 'm' : 'h').format('HHmm').split('');
// [0, 1, 2, 3].forEach(i => refs.current[i].value = time[i])
// }
} else if (e.keyCode === 40) {// down arrow
// if (i === 0 && refs.current[0].value === '0') {
// } else {
// let time = [0, 1, 2, 3].map(i => refs.current[i].value).join('')
// time = moment(time, 'HHmm').subtract(i % 2 ? 1 : 10, Math.floor(i / 2) ? 'm' : 'h').format('HHmm').split('');
// [0, 1, 2, 3].forEach(i => refs.current[i].value = time[i])
// }
}
e.preventDefault()
})
})
// input.addEventListener('keydown', e => {}))
}, [])
return <div>
<input ref={e => refs.current[0] = e} defaultValue={0}/>
<input ref={e => refs.current[1] = e} defaultValue={0}/>
<span>:</span>
<input ref={e => refs.current[2] = e} defaultValue={0}/>
<input ref={e => refs.current[3] = e} defaultValue={0}/>
</div>
}
export default Time
How can I convert commented out code to use date-fns?
You'll have to do the porting work, there's no universal way around it. Although it's reasonably easy. For instance, for the part of the code
// let time = [0, 1, 2, 3].map(i => refs.current[i].value).join('')
// time = moment(time, 'HHmm').add(i % 2 ? 1 : 10, Math.floor(i / 2) ? 'm' : 'h').format('HHmm').split('');
// [0, 1, 2, 3].forEach(i => refs.current[i].value = time[i])
it would be
let time = [0, 1, 2, 3].map(i => refs.current[i].value).join('')
const parsed = parse(time, "HHmm");
console.log("parsed", parsed)
const addF = Math.floor(i / 2) ? addMinutes : addHours;
const added = addF(parsed, i % 2 ? 1 : 10);
const formatted = format(added, "HHmm");
time = formatted.split('');
[0, 1, 2, 3].forEach(i => refs.current[i].value = time[i])
code is split into assignments for readability, but you could just chain the functions with lodash compose + date-fns/fp if you'd prefer oneliners.
Related
const [currentIndex, setCurrentIndex] = useState(0);
const [previousIndex, setPreviousIndex] = useState(2);
const [nextIndex, setNextIndex] = useState(1);
const previous = () = {
const newCurrentIndex = currentIndex === 0 ? slides.length - 1 : currentIndex - 1;
setCurrentIndex(newCurrentIndex);
const newPreviousIndex = previousIndex === 0 ? slides.length - 1 : previousIndex - 1;
setPreviousIndex(newPreviousIndex);
const newNextIndex = nextIndex === 0 ? slides.length - 1 : nextIndex - 1;
setNextIndex(newNextIndex);
}
const next = () = {
const newCurrentIndex = currentIndex === slides.length - 1 ? 0 : currentIndex + 1;
setCurrentIndex(newCurrentIndex);
const newPreviousIndex = previousIndex === slides.length - 1 ? 0 : previousIndex + 1;
setPreviousIndex(newPreviousIndex);
const newNextIndex = nextIndex === slides.length - 1 ? 0 : nextIndex + 1;
setNextIndex(newNextIndex);
}
It is used to render pictures depending on the index, the carousel displays 3 pictures at a time so I needed previous current and next index, but I don't know how to make write the logic so that I don't have to write the same thing 3 times but with different starting indexes.
You only need to state of the current index, and you can derive the other indexes from it.
To make the index cyclic (-1 becomes 2 for example), you can use a mod function.
const { useState } = React;
const mod = (x, y) => ((x % y) + y) % y;
const Demo = ({ slides }) => {
const [currentIndex, setCurrentIndex] = useState(0);
const len = slides.length;
const previous = () => {
setCurrentIndex(c => mod(c - 1, len));
}
const next = () => {
setCurrentIndex(c => mod(c + 1, len));
}
const prevIndex = mod(currentIndex - 1, len);
const nextIndex = mod(currentIndex + 1, len);
return (
<div>
<button onClick={previous}>Prev</button>
<div>Prev Index: {prevIndex}</div>
<div>Current Index: {currentIndex}</div>
<div>Next Index: {nextIndex}</div>
<button onClick={next}>Next</button>
</div>
);
};
ReactDOM
.createRoot(root)
.render(<Demo slides={[1, 2, 3]} />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
I don't think useReducer is complex enough for this use, and useReducer has some extra overhead to it, however I think you could combine your useState logic into a more complex object like
const [indexState, setIndexState] = useState({currentIndex: 0, previousIndex: 2, nextIndex: 1});
Then you can just set it like
setIndexState({currentIndex: 1, previousIndex: 0, nextIndex: 2 })
React trys to batch setStates however, this will guarentee only 1 render cycle vs your 3.
i wanted to make a game and followed a tutorial to make a Tetris game, while playing it i noticed that when you press the space button the whole game resets, i cant figure out how to fix that. My harddrop function works when i press space so that works but then the game resets to. As far as i know there should be no other function set to the spacebar (keyCode === 32)
If i remove the harddrop function the game still restarts if i press space
this is my code:
const Tetris = () => {
const [dropTime, setDropTime] = useState(null);
const [gameOver, setGameOver] = useState(false);
const [player, updatePlayerPos, resetPlayer, playerRotate] = usePlayer();
const [stage, setStage, rowsCleared] = useStage(player, resetPlayer);
const [score, setScore, rows, setRows, level, setLevel] = useGameStatus(
rowsCleared
);
console.log('re-render');
const movePlayer = dir => {
if (!checkCollision(player, stage, { x: dir, y: 0 })) {
updatePlayerPos({ x: dir, y: 0 });
}
};
const keyUp = ({ keyCode }) => {
if (!gameOver) {
// Activate the interval again when user releases down arrow.
if (keyCode === 40) {
setDropTime(1000 / (level + 1));
}
}
};
const startGame = () => {
// Reset everything
setStage(createStage());
setDropTime(1000);
resetPlayer();
setScore(0);
setLevel(0);
setRows(0);
setGameOver(false);
};
const drop = () => {
// Increase level when player has cleared 10 rows
if (rows > (level + 1) * 10) {
setLevel(prev => prev + 1);
// Also increase speed
setDropTime(1000 / (level + 1) + 200);
}
if (!checkCollision(player, stage, { x: 0, y: 1 })) {
updatePlayerPos({ x: 0, y: 1, collided: false });
} else {
// Game over!
if (player.pos.y < 1) {
console.log('GAME OVER!!!');
setGameOver(true);
setDropTime(null);
}
updatePlayerPos({ x: 0, y: 0, collided: true });
}
};
const dropPlayer = () => {
setDropTime(null);
drop();
};
const hardDrop = () => {
let pot = 0;
while (!checkCollision(player, stage, { x: 0, y: pot })) {
setDropTime(5);
pot += 1;
}
updatePlayerPos({ x: 0, y: pot-1, collided: true });
}
// starts the game
useInterval(() => {
drop();
}, dropTime);
const move = ({ keyCode }) => {
if (!gameOver) {
if (keyCode === 37) {
movePlayer(-1);
} else if (keyCode === 39) {
movePlayer(1);
} else if (keyCode === 40) {
dropPlayer();
} else if (keyCode === 38) {
playerRotate(stage, 1);
} else if (keyCode === 32) {
hardDrop()
}
}
};
return (
<StyledTetrisWrapper
role="button"
tabIndex="0"
onKeyDown={e => move(e)}
onKeyUp={keyUp}
>
<StyledHeader/>
<StyledTetris>
<Stage stage={stage} />
<aside>
{gameOver ? (
<Display gameOver={gameOver} text="Game Over" />
) : (
<div className='aside-container'>
<Display text={`Score: ${score}`} />
<Display text={`rows: ${rows}`} />
<Display text={`Level: ${level}`} />
</div>
)}
<StartButton callback={startGame}></StartButton>
<button className='back-btn'
type='button'
onClick={(e) => {
e.preventDefault();
window.location.href="https://play-it-games.netlify.app/";
}}>Back to Play it!</button>
</aside>
</StyledTetris>
</StyledTetrisWrapper>
);
};
export default Tetris;
I am using the antd-country-phone-input in my form for international phone numbers. However this does not have a mask and I want to have a masked input. I have searched for hours for a solution to this, but have not come up with any workable solution.
Any ideas how to add a mask to this input?
Code
//library
import CountryPhoneInput, {
ConfigProvider,
CountryPhoneInputValue,
} from "antd-country-phone-input";
//returning
<ConfigProvider
locale={en}
areaMapper={(area) => {
return {
...area,
emoji: (
<img
alt="flag"
style={{ width: 18, height: 18, verticalAlign: "sub" }}
src={getFlag(area.short)}
/>
),
};
}}
>
<CountryPhoneInput
id={id}
value={{
code: countryValue.code,
short: countryValue.short,
phone: countryValue.phone,
}}
onChange={(value) => {
if (value.code !== Number(countryValue.code)) {
setCountryValue({
code: value.code,
short: value.short,
phone: value.phone,
});
}
// onChange("+" + value.code!.toString() + phone);
}}
onBlur={() => setValidNumber(isValidPhoneNumber(value))}
style={{ height: "50px" }}
className="phone-height"
autoComplete="none"
placeholder={mask}
></CountryPhoneInput>
</ConfigProvider>
This is what i put together in the end to come up with a masked input for international phone numbers. It works for most countries phone numbers
const PhoneMaskWidget = (props: any) => {
const {value, onChange, id} = props;
const [countryValue, setCountryValue] = useState<CountryPhoneInputValue>({
short: "US",
code: 1,
phone: "",
});
const [mask, setMask] = useState<string>("(XXX)XXX-XXXX");
const [mounting, setMounting] = useState<boolean>(true);
const [validNumber, setValidNumber] = useState<boolean>(true);
const [countrySwitched, setCountrySwtiched] = useState<boolean>(false);
const stripPhone = (phone: any) => {
let formattedPhone = phone.replace(/[()\-. ]/g, "");
return formattedPhone;
};
const getMask = (mask: any) => {
mask = Array.isArray(mask) ? mask[1].split("#").join("X") : mask.split("#").join("X");
const firstIndex = mask.search(/\d/); // will give you the first digit in the string
let lastIndex = -1;
if (firstIndex > -1) {
for (let i = firstIndex + 1; i < mask.length; i++) {
const element = mask[i];
if (!Number(element)) {
lastIndex = i - 1;
break;
}
}
let remove = "";
if (mask[firstIndex - 1] === "(" && mask[lastIndex + 1] === ")") {
remove = mask.slice(firstIndex - 1, lastIndex + 2);
mask = mask.replace(remove, "");
setMask(mask);
}
}
return mask;
};
const getFlag = (short: string) => {
const data = require(`world_countries_lists/flags/24x24/${short.toLowerCase()}.png`);
// for dumi
if (typeof data === "string") {
return data;
}
// for CRA
return data.default;
};
const maskInput = useCallback(
(phoneValue: string, masking?: string) => {
masking = masking || mask;
let phone = stripPhone(phoneValue);
const phoneLength = stripPhone(masking).length;
if (phone.length > phoneLength) {
phone = phone.substring(0, phoneLength);
setCountryValue({ ...countryValue, phone: phone });
}
let maskedPhone = "";
let brackets = -1;
let dash = -1;
let secondDash = -1;
if (masking.indexOf("(") > -1) {
const open = masking.indexOf("(");
const close = masking.indexOf(")");
brackets = close - (open + 1);
}
if (masking.indexOf("-") > -1) {
dash = masking.indexOf("-");
}
if (masking.lastIndexOf("-") > -1) {
secondDash = masking.lastIndexOf("-");
}
if (brackets > -1) {
if (phone!.length > dash - 2) {
maskedPhone =
"(" +
phone?.substring(0, brackets) +
") " +
phone?.substring(brackets, dash - 2) +
"-" +
phone.substring(6, phone.length);
} else if (phone!.length > brackets && brackets >= 0) {
maskedPhone =
"(" +
phone?.substring(0, brackets) +
") " +
phone.substring(brackets, phone.length);
} else {
maskedPhone = phone;
}
} else {
if (phone.length > secondDash - 1 && secondDash > -1 && secondDash !== dash) {
maskedPhone =
phone.substring(0, dash) +
"-" +
phone.substring(dash, secondDash - 1) +
"-" +
phone.substring(secondDash - 1, phone.length);
} else if (phone.length > dash && dash > -1) {
maskedPhone =
phone.substring(0, dash) + "-" + phone.substring(dash, phone.length);
} else {
maskedPhone = phone;
}
}
return maskedPhone;
},
[mask]
);
const parsePhoneNumber = useCallback(
(value: string) => {
if (!value) return;
let phone = phoneparser.parse(value);
const number = phone?.localized?.stripped || phone.stripped;
const countryShort = phone?.country?.iso3166?.alpha2;
if (!countryShort) {
return;
}
const country = countries.find((c: any) => c.iso === countryShort);
const mask = getMask(country.mask);
setMask(mask);
if (countryShort && number && country.code) {
setCountryValue({
short: countryShort,
code: country.code,
phone: maskInput(number.slice(-stripPhone(mask).length), mask),
});
}
},
[maskInput]
);
useEffect(() => {
if (!countryValue) return;
if (!mounting) {
const country = countries.find((c: any) => c.iso === countryValue.short);
setMask(getMask(country.mask));
setCountryValue({
...countryValue,
phone: countrySwitched ? "" : countryValue.phone,
});
setCountrySwtiched(false);
}
if (mounting) {
if (value) parsePhoneNumber(value);
setMounting(false);
}
}, [countryValue.short, countrySwitched, mounting, value, parsePhoneNumber]);
return (
<ConfigProvider
locale={en}
areaMapper={(area) => {
return {
...area,
emoji: (
<img
alt="flag"
style={{ width: 18, height: 18, verticalAlign: "sub" }}
src={getFlag(area.short)}
/>
),
};
}}
>
<CountryPhoneInput
id={id}
value={{
code: countryValue.code,
short: countryValue.short,
phone: !value || value === "" ? "" : countryValue.phone,
}}
onChange={(value) => {
if (value.short !== countryValue.short) {
setCountrySwtiched(true);
}
const maskedPhone = maskInput(value.phone ? value.phone : "");
setCountryValue({
code: value.code,
short: value.short,
phone: maskedPhone,
});
onChange("+" + value.code!.toString() + " " + maskedPhone);
// onChange("+" + value.code!.toString() + stripPhone(maskedPhone));
}}
onBlur={() => {
const maskedPhone = maskInput(countryValue.phone!);
setValidNumber(
isValidPhoneNumber("+ " + countryValue.code + stripPhone(maskedPhone))
);
}}
style={{ height: "50px" }}
className="phone-height"
autoComplete="none"
placeholder={mask}
></CountryPhoneInput>
</ConfigProvider>
);
};
export default PhoneMaskWidget;
I faced an issue with react-p5-wrapper that it is running in background although I have switch to another route in my react app.
For example, I am currently in /game, and the console is logging "running draw", but when I switch to /about-us, it still logging, meaning it is still running the draw function
Here is my code in sandbox
App.js
import "./styles.css";
import { Route, Switch, BrowserRouter as Router, Link } from "react-router-dom";
export default function App() {
return (
<div className="App">
<Router>
<Link to="/game">Game</Link> | <Link to="/about-us">About Us</Link>
<Switch>
<Route path="/about-us" component={require("./abtus").default} />
<Route path="/game" component={require("./game").default} />
</Switch>
</Router>
</div>
);
}
game.js
import { useEffect } from "react";
import { ReactP5Wrapper, P5Instance } from "react-p5-wrapper";
// Sound
let pointSound, endSound;
let playEndSound = false;
/**
* #param {P5Instance} p
*/
const sketch = (p) => {
const MAX_SPEED = 15;
const pickDirections = () => {
return ((Math.floor(Math.random() * 3) % 2 === 0 ? 1 : -1) * (Math.floor(Math.random() * 2) + 1));
};
const randomXPos = () => {
return p.random(30, p.width - 30);
};
const ramdomImgIndex = () => {
return Math.floor(Math.random() * imgs.length);
};
const reset = () => {
score = 0;
speed = 2;
falls = [
{
y: -70,
x: randomXPos(),
rotation: 0,
direction: pickDirections(),
imgIndex: ramdomImgIndex()
}
];
};
const rotate_n_draw_image = (image,img_x,img_y,img_width,img_height,img_angle) => {
p.imageMode(p.CENTER);
p.translate(img_x + img_width / 2, img_y + img_width / 2);
p.rotate((Math.PI / 180) * img_angle);
p.image(image, 0, 0, img_width, img_height);
p.rotate((-Math.PI / 180) * img_angle);
p.translate(-(img_x + img_width / 2), -(img_y + img_width / 2));
p.imageMode(p.CORNER);
};
// Images
let imgs = [],
basket = { img: null, width: 150, height: 150 },
imgSize = 50;
let screen = 0,
falls = [
{
y: -70,
x: randomXPos(),
rotation: 0,
direction: pickDirections(),
imgIndex: ramdomImgIndex()
}
],
score = 0,
speed = 2;
const startScreen = () => {
p.background(205, 165, 142);
p.fill(255);
p.textAlign(p.CENTER);
p.text("WELCOME TO MY CATCHING GAME", p.width / 2, p.height / 2);
p.text("click to start", p.width / 2, p.height / 2 + 20);
reset();
};
const gameOn = () => {
p.background(219, 178, 157);
p.textAlign(p.LEFT);
p.text("score = " + score, 30, 20);
falls = falls.map(({ x, y, rotation, direction, imgIndex }) => {
// rotate while dropping
rotation += direction;
// dropping
y += speed;
return { x, y, rotation, direction, imgIndex };
});
falls.forEach(({ x, y, rotation, imgIndex }, i) => {
// when is lower than the border line
if (y > p.height) {
screen = 2;
playEndSound = true;
}
// when reaching the border line and is within the range
if (
y > p.height - 50 &&
x > p.mouseX - basket.width / 2 &&
x < p.mouseX + basket.width / 2
) {
// Play Sound
pointSound.currentTime = 0;
pointSound.play();
// Increase Score
score += 10;
// Increase Speed
if (speed < MAX_SPEED) {
speed += 0.1;
speed = parseFloat(speed.toFixed(2));
}
// Whether add new item into array or not
if (i === falls.length - 1 && falls.length < 3) {
falls.push({
x: randomXPos(),
y: -70 - p.height / 3,
rotation: 0,
direction: pickDirections(),
imgIndex: ramdomImgIndex()
});
}
falls[i].y = -70;
falls[i].x = randomXPos();
falls[i].imgIndex = ramdomImgIndex();
}
rotate_n_draw_image(imgs[imgIndex], x, y, imgSize, imgSize, rotation);
});
p.imageMode(p.CENTER);
p.image(
basket.img,
p.mouseX,
p.height - basket.height / 2,
basket.width,
basket.height
);
};
const endScreen = () => {
if (playEndSound) {
endSound.play();
playEndSound = false;
}
p.background(205, 165, 142);
p.textAlign(p.CENTER);
p.text("GAME OVER", p.width / 2, p.height / 2);
p.text("SCORE = " + score, p.width / 2, p.height / 2 + 20);
p.text("click to play again", p.width / 2, p.height / 2 + 60);
};
p.preload = () => {
// Load Images
imgs[0] = p.loadImage("https://dummyimage.com/400x400");
imgs[1] = p.loadImage("https://dummyimage.com/400x400");
imgs[2] = p.loadImage("https://dummyimage.com/401x401");
basket.img = p.loadImage("https://dummyimage.com/500x500");
};
p.setup = () => {
p.createCanvas(
window.innerWidth > 400 ? 400 : window.innerWidth,
window.innerHeight > 500 ? 500 : window.innerHeight
);
};
p.draw = () => {
console.log("running draw");
switch (screen) {
case 0:
startScreen();
break;
case 1:
gameOn();
break;
case 2:
endScreen();
break;
default:
}
};
p.mousePressed = () => {
if (screen === 0) {
screen = 1;
} else if (screen === 2) {
screen = 0;
}
};
};
const CatchingGmae = () => {
useEffect(() => {
// eslint-disable-next-line
pointSound = new Audio("/game/points.wav");
// eslint-disable-next-line
endSound = new Audio("/game/end.wav");
pointSound.volume = 0.3;
return () => {
pointSound.muted = true;
endSound.muted = true;
};
});
return (
<div className="mx-auto flex justify-center items-center">
<ReactP5Wrapper sketch={sketch} />
</div>
);
};
export default CatchingGame;
Is there anyway to stop it from running in background when user switches route?
Given your setup, I can see two ways of telling the sketch to stop when route is switched and the Game react component is not rendered anymore.
Alt 1. You can make something similar to react-p5-wrapper
documentation, reacting to props:
In CatchingGmae component:
const [lastRender, setLastRender] = useState(Date.now());
useEffect(() => {
const interval = setInterval(() => setLastRender(Date.now()), 100);
return () => {
clearInterval(interval);
};
}, []);
return (
<>
<div className="mx-auto flex justify-center items-center">
<ReactP5Wrapper sketch={sketch} lastRender={lastRender} />
In sketch:
let lastRender = 0;
p.updateWithProps = (props) => {
lastRender = props.lastRender;
};
p.draw = () => {
if (!(Date.now() > lastRender + 100)) {
console.log("running draw");
☝ The problem with the Alt 1 is that react will do calculations and re-render frequently for no reason.
Alt 2. Use a state outside of React, a very simple side-effect
for the component, for the sketch to poll on.
Add to CatchingGmae component:
useEffect(() => {
window.noLoop = false;
return () => {
window.noLoop = true;
};
}, []);
Inside p.draw:
if (window.noLoop) return p.noLoop();
☝ This works without calculations, but you might want to scope the global within your own namespace or using other state manager.
I am trying to implement Conway's Game of Life in React, but it is freezing whenever a new generation is called. I assume this is because there is too much overhead caused by constantly re-rendering the DOM, but I don't know how to resolve this, nor can I think of an alternative to simply posting my entire code, so I apologise in advance for the verbosity.
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import styled from "styled-components"
interface TileProps {
bool: boolean
}
const Tile: React.FC<TileProps> = ({bool}) => {
const colour = bool == true ? "#00FF7F" : "#D3D3D3"
return (
<div style = {{backgroundColor: colour}}/>
)
}
interface GridProps {
cells: boolean[][]
}
const StyledGrid = styled.div`
display: grid;
grid-template-columns: repeat(100, 1%);
height: 60vh;
width: 60vw;
margin: auto;
position: relative;
background-color: #E182A8;
`
const Grid: React.FC<GridProps> = ({cells}) => {
return (
<StyledGrid>
{cells.map(row => row.map(el => <Tile bool = {el}/>))}
</StyledGrid>
)
}
const randomBoolean = (): boolean => {
const states = [true, false];
return states[Math.floor(Math.random() * states.length)]
}
const constructCells = (rows: number, columns: number): boolean[][] => {
return constructEmptyMatrix(rows, columns).map(row => row.map(e => randomBoolean()))
}
const constructEmptyMatrix = (rows: number, columns: number): number[][] => {
return [...Array(rows)].fill(0).map(() => [...Array(columns)].fill(0));
}
const App: React.FC = () => {
const columns = 100;
const rows = 100;
const [cells, updateCells] = useState<boolean[][]>(constructCells(rows, columns));
useEffect(() => {
const interval = setInterval(() => {
newGeneration();
}, 1000);
return () => clearInterval(interval);
}, []);
const isRowInGrid = (i: number): boolean => 0 <= i && i <= rows - 1
const isColInGrid = (j : number): boolean => 0 <= j && j <= columns -1
const isCellInGrid = (i: number, j: number): boolean => {
return isRowInGrid(i) && isColInGrid(j)
}
const numberOfLiveCellNeighbours = (i: number, j: number): number => {
const neighbours = [
[i - 1, j], [i, j + 1], [i - 1, j + 1], [i - 1, j + 1],
[i + 1, j], [i, j - 1], [i + 1, j - 1], [i + 1, j + 1]
]
const neighboursInGrid = neighbours.filter(neighbour => isCellInGrid(neighbour[0], neighbour[1]))
const liveNeighbours = neighboursInGrid.filter(x => {
const i = x[0]
const j = x[1]
return cells[i][j] == true
})
return liveNeighbours.length;
}
const updateCellAtIndex = (i: number, j: number, bool: boolean) => {
updateCells(oldCells => {
oldCells = [...oldCells]
oldCells[i][j] = bool;
return oldCells;
})
}
const newGeneration = (): void => {
cells.map((row, i) => row.map((_, j) => {
const neighbours = numberOfLiveCellNeighbours(i, j);
if (cells[i][j] == true){
if (neighbours < 2){
updateCellAtIndex(i, j, false);
} else if (neighbours <= 3){
updateCellAtIndex(i, j, true);
}
else {
updateCellAtIndex(i, j, false);
}
} else {
if (neighbours === 3){
updateCellAtIndex(i, j, true);
}
}
}))
}
return (
<div>
<Grid cells = {cells}/>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'));
The application freezes because React does not batch your individual state updates. More information about this can be found in this answer
You have two options here.
Use ReactDOM.unstable_batchedUpdates:
This can be done with a single line change, but note that the method is not part of the public API
useEffect(() => {
const interval = setInterval(() => {
// wrap generation function into batched updates
ReactDOM.unstable_batchedUpdates(() => newGeneration())
}, 1000);
return () => clearInterval(interval);
}, []);
Update all states in one operation.
You could refactor your code to set updated cells only once. This option does not use any unstable methods
useEffect(() => {
const interval = setInterval(() => {
// `newGeneration` function needs to be refactored to remove all `updateCells` calls. It should update the input array and return the result
const newCells = newGeneration(oldCells);
// there will be only one call to React on each interval
updateCells(newCells);
}, 1000);
return () => clearInterval(interval);
}, []);