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);
}, []);
Related
I am making a 3D walkable world. Implemented the keyboard movement using wasd and space and mouse movement. I have no problem with the mouse movement, but when pressing the keys on the keyboard it doesnt work. I have seen the example multiple times and it works, but for me it doesnt seem so.
I have a useKeyboardInput.js where I add the event listeners and it registers when i`m pressing the key.
import { useCallback, useEffect, useState } from "react";
export const useKeyboardInput = (keysToListen = []) => {
const getKeys = useCallback(() => {
const lowerCaseArray = [];
const hookReturn = {};
keysToListen.forEach((key) => {
const lowerCaseKey = key.toLowerCase();
lowerCaseArray.push(lowerCaseKey);
hookReturn[lowerCaseKey] = false;
});
return {
lowerCaseArray,
hookReturn,
};
}, [keysToListen]);
const [keysPressed, setPressedKeys] = useState(getKeys().hookReturn);
useEffect(() => {
const handleKeyDown = (e) => {
const lowerKey = e.key.toLowerCase();
if (getKeys().lowerCaseArray.includes(lowerKey)) {
setPressedKeys((keysPressed) => ({ ...keysPressed, [lowerKey]: true }));
}
console.log("Pressed Key is: " + e.key);
};
const handleKeyUp = (e) => {
const lowerKey = e.key.toLowerCase();
if (getKeys().lowerCaseArray.includes(lowerKey)) {
setPressedKeys((keysPressed) => ({
...keysPressed,
[lowerKey]: false,
}));
}
};
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("keyup", handleKeyUp);
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("keyup", handleKeyUp);
};
}, [keysToListen, getKeys]);
console.log("KeysPressed is: " + JSON.stringify(keysPressed));
return keysPressed;
};
And then i have the Player.js where i am using the hooks for keyboard and mouse.
import { useSphere } from "#react-three/cannon";
import React, { useEffect, useRef, useState } from "react";
import { useFrame, useThree } from "#react-three/fiber";
import { Vector3 } from "three";
import { useKeyboardInput } from "../Hooks/useKeyboardInput";
import { useMouseInput } from "../Hooks/useMouseInput";
import { useVariable } from "../Hooks/useVariable";
import { Bullet } from "./Bullet";
import { Raycaster } from "three";
/** Player movement constants */
const speed = 300;
const bulletSpeed = 30;
const bulletCoolDown = 300;
const jumpSpeed = 5;
const jumpCoolDown = 400;
export const Player = () => {
/** Player collider */
const [sphereRef, api] = useSphere(() => ({
mass: 100,
fixedRotation: true,
position: [0, 1, 0],
args: [0.2],
material: {
friction: 0,
},
}));
/** Bullets */
const [bullets, setBullets] = useState([]);
/** Input hooks */
const pressed = useKeyboardInput(["w", "a", "s", "d", " "]);
const pressedMouse = useMouseInput();
/** Converts the input state to ref so they can be used inside useFrame */
const input = useVariable(pressed);
const mouseInput = useVariable(pressedMouse);
/** Player movement constants */
const { camera, scene } = useThree();
/** Player state */
const state = useRef({
timeToShoot: 0,
timeTojump: 0,
vel: [0, 0, 0],
jumping: false,
});
useEffect(() => {
api.velocity.subscribe((v) => (state.current.vel = v));
}, [api]);
/** Player loop */
useFrame((_, delta) => {
/** Handles movement */
console.log("Input.current is: " + JSON.stringify(input.current));
const { w, s, a, d } = input.current;
const space = input.current[" "];
let velocity = new Vector3(0, 0, 0);
let cameraDirection = new Vector3();
camera.getWorldDirection(cameraDirection);
let forward = new Vector3();
forward.setFromMatrixColumn(camera.matrix, 0);
forward.crossVectors(camera.up, forward);
let right = new Vector3();
right.setFromMatrixColumn(camera.matrix, 0);
let [horizontal, vertical] = [0, 0];
if (w == true) {
vertical += 1;
console.log("Pressed w");
} else if (s == true) {
vertical -= 1;
console.log("Pressed s");
} else if (d == true) {
horizontal += 1;
console.log("Pressed d");
} else if (a == true) {
horizontal -= 1;
console.log("Pressed a");
}
console.log(
"Horizontal is: " + horizontal + " and vertical is: " + vertical
);
if (horizontal !== 0 && vertical !== 0) {
velocity
.add(forward.clone().multiplyScalar(speed * vertical))
.add(right.clone().multiplyScalar(speed * horizontal));
velocity.clampLength(-speed, speed);
} else if (horizontal !== 0) {
velocity.add(right.clone().multiplyScalar(speed * horizontal));
} else if (vertical !== 0) {
velocity.add(forward.clone().multiplyScalar(speed * vertical));
}
console.log("velocity is: " + JSON.stringify(velocity));
/** Updates player velocity */
api.velocity.set(
velocity.x * delta,
state.current.vel[1],
velocity.z * delta
);
/** Updates camera position */
camera.position.set(
sphereRef.current.position.x,
sphereRef.current.position.y + 1,
sphereRef.current.position.z
);
/** Handles jumping */
if (state.current.jumping && state.current.vel[1] < 0) {
/** Ground check */
const raycaster = new Raycaster(
sphereRef.current.position,
new Vector3(0, -1, 0),
0,
0.2
);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length !== 0) {
state.current.jumping = false;
}
}
if (space && !state.current.jumping) {
const now = Date.now();
if (now > state.current.timeTojump) {
state.current.timeTojump = now + jumpCoolDown;
state.current.jumping = true;
api.velocity.set(state.current.vel[0], jumpSpeed, state.current.vel[2]);
}
}
/** Handles shooting */
const bulletDirection = cameraDirection.clone().multiplyScalar(bulletSpeed);
const bulletPosition = camera.position
.clone()
.add(cameraDirection.clone().multiplyScalar(2));
if (mouseInput.current.left) {
const now = Date.now();
if (now >= state.current.timeToShoot) {
state.current.timeToShoot = now + bulletCoolDown;
setBullets((bullets) => [
...bullets,
{
id: now,
position: [bulletPosition.x, bulletPosition.y, bulletPosition.z],
forward: [bulletDirection.x, bulletDirection.y, bulletDirection.z],
},
]);
}
}
});
return (
<>
{/** Renders bullets */}
{bullets.map((bullet) => {
return (
<Bullet
key={bullet.id}
velocity={bullet.forward}
position={bullet.position}
/>
);
})}
</>
);
};
It registers the keys that are being pressed but nothing happens.
At first glance, the following snippet seems to be the source of issue.
const getKeys = useCallback(() => {
const lowerCaseArray = [];
const hookReturn = {};
You may have to move those variables outside of the callback.
both handleKeyDown and handleKeyUp calls getKeys() which creates new empty values
so the problem is that when i'm trying to select an area using leaflet and click somewhere outside of selection tab it removes focus from the selection and after trying to select it doesn't work as intended meaning that i can't select properly
how do i return focus back to the tab?
appreciate any suggestions, if need to clarify something - tell me about it
here are the screenshots of how it should be and how it actually is
EDIT: here's code
import React, { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import L, { FeatureGroup, LatLngBounds, LeafletEvent, LeafletEventHandlerFn, LeafletMouseEvent, Point, PointTuple, Map } from 'leaflet';
import 'leaflet/dist/leaflet.css';
import 'leaflet-area-select';
import { Box, IconButton } from '#material-ui/core';
import GpsNotFixedRoundedIcon from '#material-ui/icons/GpsNotFixedRounded';
import { useStyles } from './styles';
import { disableMap, drawGrid } from '../../../utils/MapHelpers';
import { NewObs, Observation, Panorama } from '../../../state/reducers/types';
import { clearObs, saveGridSize, saveObsBounds, selectObsArea } from '../../../state/actions/inspector';
import OrientationMap from '../OrientationMap';
export type Grid = { gridCoordX: number[]; gridCoordY: number[]; gridLayer: FeatureGroup };
export let inspMap: Map;
interface AreaSelectEvent extends LeafletEvent {
bounds: LatLngBounds;
}
interface ILeafletWrapper {
handleObsForm: (showObsForm: boolean) => void;
handleIsSelectingObs: (isSelecting: boolean) => void;
handleOrientationMap: () => void;
handleGridSizeSelecting: () => void;
isSelectingObs: boolean;
newObs: NewObs;
observations: Observation[];
showOrientationMap: boolean;
isSelectingGridSize: boolean;
panorama: Panorama;
}
export const LeafletWrapper = (props: ILeafletWrapper) => {
const {
isSelectingObs,
handleObsForm,
handleIsSelectingObs,
observations,
showOrientationMap,
handleOrientationMap,
isSelectingGridSize,
handleGridSizeSelecting,
panorama,
} = props;
const { urlTemplates, originalResolution, originalZoomLevel, gridInfo } = panorama;
const [{ gridCoordX, gridCoordY, gridLayer }, setGrid] = useState<Grid>({
gridCoordX: [0],
gridCoordY: [0],
gridLayer: {},
} as Grid);
const [gridSizeBounds, setGridSizeBounds] = useState<LatLngBounds | undefined>();
const classes = useStyles();
const dispatch = useDispatch();
// initialization of Leaflet
useEffect(() => {
inspMap = new L.Map('map', { zoomControl: false, maxBoundsViscosity: 0.9 });
inspMap.setView(inspMap.unproject([originalResolution.x / 2, originalResolution.y / 2], originalZoomLevel), originalZoomLevel);
// you just need to pass something as url, further work with url is rewritten
const TileLayer = L.tileLayer('url', {
// lock to only zoom level for first version of app
minZoom: originalZoomLevel,
maxZoom: originalZoomLevel,
noWrap: true,
tileSize: 8192,
});
// custom internal method to get tile's url, have to use this method due to the inability of
// generating a templated URL from a predefined URLs (they are all unique)
TileLayer.getTileUrl = ({ z, x, y }: { z: number; x: number; y: number }) => {
return urlTemplates[y + '-' + x];
};
TileLayer.addTo(inspMap);
// disable the need to use a ctrl btn to select area
inspMap.selectArea.setControlKey(false);
// draw grid if there is cached panoramas info
if (gridInfo.x) {
const bounds = L.latLngBounds(
inspMap.unproject([0, 0], originalZoomLevel),
inspMap.unproject([gridInfo.x, gridInfo.y], originalZoomLevel),
);
const gridObj = drawGrid(bounds, inspMap, originalZoomLevel, originalResolution);
setGrid(gridObj);
setGridSizeBounds(bounds);
}
return () => {
inspMap.remove();
dispatch(clearObs());
};
}, []);
// disable interactions with leaflet while selectArea is enabled
useEffect(() => {
disableMap(isSelectingObs, inspMap);
if ((isSelectingObs || isSelectingGridSize) && !inspMap.selectArea.enabled()) {
inspMap.selectArea.enable();
inspMap.getContainer().style.cursor = 'crosshair';
inspMap.on('areaselected', handleObsAreaSelected as LeafletEventHandlerFn);
} else {
inspMap.getContainer().style.cursor = 'grab';
}
}, [isSelectingObs, isSelectingGridSize]);
// reset grid
useEffect(() => {
if (isSelectingGridSize && gridLayer?.remove) {
gridLayer.remove();
setGridSizeBounds(undefined);
}
}, [isSelectingGridSize, gridLayer]);
// draw observations
useEffect(() => {
// eslint-disable-next-line
observations.map((obs: any) => {
const index = obs.id;
if (obs.position.bounds) return;
const bounds = L.latLngBounds(
inspMap.unproject(obs.position.leftTopPoint, originalZoomLevel),
inspMap.unproject(obs.position.rightBottomPoint, originalZoomLevel),
);
const rectangle = L.rectangle(bounds, { color: '#ff9200', weight: 2, fillOpacity: 0 }).addTo(inspMap);
dispatch(saveObsBounds({ bounds: { remove: rectangle.remove.bind(rectangle) }, index }));
});
}, [observations]);
const handleObsAreaSelected = (e: AreaSelectEvent) => {
if (isSelectingObs) {
const leftTopPoint = inspMap.project(e.bounds.getNorthWest(), originalZoomLevel);
leftTopPoint.x = Math.round(leftTopPoint.x);
leftTopPoint.y = Math.round(leftTopPoint.y);
const rightBottomPoint = inspMap.project(e.bounds.getSouthEast(), originalZoomLevel);
rightBottomPoint.x = Math.round(rightBottomPoint.x);
rightBottomPoint.y = Math.round(rightBottomPoint.y);
const bounds = L.rectangle(e.bounds, { color: '#ff9200', weight: 2, fillOpacity: 0 });
bounds.addTo(inspMap);
dispatch(selectObsArea({ leftTopPoint, rightBottomPoint, bounds: { remove: bounds.remove.bind(bounds) } }));
handleObsForm(true);
handleIsSelectingObs(false);
inspMap.selectArea.disable();
inspMap.removeEventListener('areaselected', handleObsAreaSelected as LeafletEventHandlerFn);
}
if (isSelectingGridSize) {
handleGridSizeSelecting();
const gridObj = drawGrid(e.bounds, inspMap, originalZoomLevel, originalResolution);
setGrid(gridObj);
setGridSizeBounds(e.bounds);
inspMap.selectArea.disable();
inspMap.removeEventListener('areaselected', handleObsAreaSelected as LeafletEventHandlerFn);
dispatch(saveGridSize({ x: gridObj.gridCoordX[0], y: gridObj.gridCoordY[0] }));
}
};
const panToCell = ({ latlng }: LeafletMouseEvent) => {
// user click coordinates converted to pixel relative coordinate system for original zoom level
const point: Point = inspMap.project(latlng, originalZoomLevel);
const cellCenter = [];
// find center of cell
for (let i = 0; i < gridCoordX.length; i++) {
if (point.x < gridCoordX[i]) {
cellCenter.push(gridCoordX[0] / 2);
break;
}
if (point.x > gridCoordX[i] && (point.x < gridCoordX[i + 1] || i === gridCoordX.length - 1)) {
cellCenter.push(gridCoordX[i] + gridCoordX[0] / 2);
break;
}
}
for (let i = 0; i < gridCoordY.length; i++) {
if (point.y < gridCoordY[i]) {
cellCenter.push(gridCoordY[0] / 2);
break;
}
if (point.y > gridCoordY[i] && (point.y < gridCoordY[i + 1] || i === gridCoordY.length - 1)) {
cellCenter.push(gridCoordY[i] + gridCoordY[0] / 2);
break;
}
}
// temporary locked zoom level due only one zoom level available
inspMap.flyTo(inspMap.unproject(cellCenter as PointTuple, originalZoomLevel), originalZoomLevel);
};
const panToCenter = () => inspMap.panTo(inspMap.unproject([originalResolution.x / 2, originalResolution.y / 2], originalZoomLevel));
const zoomIn = () => inspMap.setZoom(inspMap.getZoom() + 1);
const zoomOut = () => inspMap.setZoom(inspMap.getZoom() - 1);
return (
<Box className={classes.root} height="100%">
<div id="map" style={{ height: '100%', width: '100%' }} />
<Box>
<IconButton className={classes.goToCenterBtn} aria-label="delete" onClick={panToCenter}>
<GpsNotFixedRoundedIcon color="inherit" />
</IconButton>
</Box>
<OrientationMap
panToCell={panToCell}
observations={observations}
showOrientationMap={showOrientationMap}
handleOrientationMap={handleOrientationMap}
handlePanToCenter={panToCenter}
zoomIn={zoomIn}
zoomOut={zoomOut}
gridSizeBounds={gridSizeBounds}
panorama={panorama}
/>
</Box>
);
};
I'm building a component that should show elements of an array, one per time and it shows the element for a period of time determined by the data. With the data below I would like to show textA for 5 seconds and then change to textB which I would show for 20 seconds, and then show textC for 10 seconds and start over again, showing textA and so on.
Here is what I'm trying:
const slides = [
{text: 'textA, time_length: 5},
{text: 'textB', time_length: 20},
{text: 'textC', time_length: 10}
] ;
const play = () => {
if (slides && slides.length > 0) {
const playSlides = () => {
this.timer = setTimeout(() => {
// update index
if (currentIndex + 1 < splashes.length) {
setCurrentIndex(currentIndex + 1)
} else {
setCurrentIndex(0)
}
}, slides[currentIndex])
}
}
}
useEffect(() => {
if (slides.length) {
debugger
play(slides)
}
return clearTimeout(this.timer)
}, [slides])
return <p>{slides[currentIndex]}</p>
}
This solution will show the slide.text every slide.time_length.
Notice that the 'loop' is achieved via the dependency [currentIndex]; in other words, every time the currentIndex changes, the useEffect will run once more, and will update the currentIndex after the next time_length seconds.
The calculation (currentIndex + 1) % slides.length will reset the index back to 0 if it overflows the array length (it's just a shortened version of your conditional, nothing special).
const slides = [
{ text: 'textA', time_length: 5 },
{ text: 'textB', time_length: 20 },
{ text: 'textC', time_length: 10 }
];
const TextSlider = () => {
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
if (slides.length) {
const timeoutId = setTimeout(() => {
setCurrentIndex((currentIndex + 1) % slides.length)
}, slides[currentIndex].time_length * 1000)
return () => clearTimeout(timeoutId);
}
return () => { }
}, [currentIndex])
return <h1>{slides[currentIndex].text}</h1>
}
Sometimes being less wordy can make it easier to see what's going on. Your mileage may vary -
import { useState, useEffect } from "react"
function TextSlider ({ slides = [] }) {
const [i, nextSlide] =
useState(0)
useEffect(() => {
if (i >= slides.length) return
const t =
setTimeout
( nextSlide
, slides[i].time_length * 1000
, (i + 1) % slides.length
)
return () => clearTimeout(t)
}, [i])
return <h1>{slides[i].text}</h1>
}
ReactDom.render(document.body, <TextSlider slides={slides} />)
I am learning react and decided to try create a sorting visualizer. I started with bubble sort and pretty much succeeded creating a basic visualizer. setTimeout was the main function that I used to apply the visualization.
I felt that relaying on setTimeout does not utilize react enough and I wanted to try different approach, applying the visualization with useState hook and the rerendering that is happening when changing the state. I understand that useState hook is asynchronous, and will not immediately reflect.
Here is my code:
import React, { useContext, useState, useEffect } from 'react';
const NUMBER_OF_ELEMENTS = 10;
const DEFAULT_COLOR = 'black';
const randomIntFromInterval = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
}
const Dummy = () => {
const [arr, setArr] = useState([]);
const [numberOfElements, setNumberOfElements] = useState(NUMBER_OF_ELEMENTS);
const [doneElements, setDoneElements] = useState([]);
useEffect(() => {
resetArray();
}, []);
const resetArray = () => {
const arr1 = [];
for(let i = 0; i < numberOfElements; i++)
{
arr1[i] = randomIntFromInterval(5, 100);
}
console.log(arr1);
setArr(arr1);
}
const bubbleSort = (arr, n) => {
let i, j, temp, swapped, delay = 1;
for(i = 0; i < n - 1; i++)
{
swapped = false;
for(j = 0; j < n - i - 1; j++)
{
createColor(j, j + 1, delay++, 'darkred');
if(arr[j] > arr[j + 1])
{
// swap arr[j] and arr[j+1]
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true;
createAnimation(j, j + 1, delay++);
}
createColor(j, j + 1, delay++, 'black');
}
createSingleColor(n - i - 1, delay++, 'green');
// If no two elements were
// swapped by inner loop, then break
if(swapped === false) break;
}
for(let k = 0; k < n - i - 1; k++) {
createSingleColor(k, delay++, 'green');
}
}
const createAnimation = (one, two, delay) => {
const arrayBars = document.getElementsByClassName('array-bar');
setTimeout(() => {
const barOneHeight = arrayBars[one].style.height;
const barTwoHeight = arrayBars[two].style.height;
arrayBars[two].style.height = `${barOneHeight}`;
arrayBars[one].style.height = `${barTwoHeight}`;
}, 250 * delay);
}
const createColor = (one, two, delay, color) => {
const arrayBars = document.getElementsByClassName('array-bar');
setTimeout(() => {
arrayBars[two].style.backgroundColor = color;
arrayBars[one].style.backgroundColor = color;
}, 250 * delay);
}
const createSingleColor = (index, delay, color) => {
const arrayBars = document.getElementsByClassName('array-bar');
setTimeout(() => {
arrayBars[index].style.backgroundColor = color;
}, 250 * delay);
}
const handleSort = (arr) => {
bubbleSort(arr, arr.length);
}
const handlerRange = (e) => {
setNumberOfElements(e.target.value);
}
return (
<div>
<div className="array-container">
{arr.map((value, idx) => (
<div className="array-bar"
key={idx}
style={{
backgroundColor: 'black',
height: `${value}px`,
width: `${100 / arr.length}%`,
display: 'inline-block',
margin: '0 1px'
}}>
</div>
))}
</div>
<div className="buttons-container">
<button onClick={() => handleSort(arr)}>Sort!</button>
<button onClick={() => resetArray()}>Reset</button>
<button onClick={() => {
setDoneElements([...doneElements, 7]);
console.log(doneElements);}}>print</button>
</div>
<div className="slider-container">
1
<input type="range"
min="1"
max="100"
onChange={(e) => handlerRange(e)}
className="slider"
id="myRange"
/>
100
</div>
{numberOfElements}
</div>
);
}
export default Dummy;
For example when I tried using the setDoneElements in the bubblesort function I messed up the visualization.
Is there a way to use hooks to apply the visualization, and not to rely on setTimeout that much?
Found a solution:
import React, { useState, useEffect } from 'react';
const shortid = require('shortid');
const randomIntFromInterval = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
}
const Dummy2 = () => {
const [arr, setArr] = useState([]);
const [length, setLength] = useState(10);
const [doneElements, setDoneElements] = useState([]);
const [compareElements, setCompareElements] = useState([]);
useEffect(() => {
generateArray();
}, [length]);
const generateArray = () => {
setDoneElements([]);
setCompareElements([]);
const tempArr = [];
for(let i = 0; i < length; i++) {
tempArr.push(randomIntFromInterval(7, 107));
}
setArr([...tempArr]);
}
const handleLength = (e) => {
setLength(e.target.value);
}
const bubbleSort = () => {
let i, j, swapped, delay = 100;
const tempArr = [...arr];
const tempDoneElements = [...doneElements];
for(i = 0; i < length - 1; i++)
{
swapped = false;
for(j = 0; j < length - i - 1; j++)
{
createColor([j, j + 1], delay, 'COMPARE');
delay += 100;
if(tempArr[j] > tempArr[j + 1])
{
createAnimation(tempArr, j, j + 1, delay);
delay += 100;
swapped = true;
}
createColor([], delay, 'NONE');
delay += 100;
}
tempDoneElements.push(length - i - 1);
createColor(tempDoneElements, delay, 'DONE');
delay += 100;
// If no two elements were
// swapped by inner loop, then break
if(swapped === false) break;
}
for(let k = 0; k < length - i - 1; k++) {
tempDoneElements.push(k);
}
createColor(tempDoneElements, delay, 'DONE');
delay += 100;
}
const createAnimation = (tempArr, indexOne, indexTwo, delay) => {
const temp = tempArr[indexOne];
tempArr[indexOne] = tempArr[indexTwo];
tempArr[indexTwo] = temp;
const newArr = [...tempArr];
setTimeout(() => {
setArr([...newArr]);
}, delay);
}
const createColor = (tempElements, delay, action) => {
switch(action) {
case 'DONE':
const newDoneElements = [...tempElements];
setTimeout(() => {
setDoneElements([...newDoneElements]);
}, delay);
break;
case 'COMPARE':
setTimeout(() => {
setCompareElements([...tempElements]);
}, delay);
break;
default:
setTimeout(() => {
setCompareElements([]);
}, delay);
}
}
const maxVal = Math.max(...arr);
return (
<div>
<div className="array-container" style={{height: '50%'}}>
{arr.map((value, idx) => (
<div className="array-element"
key={shortid.generate()}
style={{height: `${(value * 100 / maxVal).toFixed()}%`,
width: `calc(${100 / length}% - 2px)`,
margin: '0 1px',
display: 'inline-block',
backgroundColor: compareElements.includes(idx) ? 'darkred' :
doneElements.includes(idx) ? 'green' : 'black',
color: 'white'}}
></div>))
}
</div>
<div>
<button onClick={() => generateArray()}>New array</button>
<button onClick={() => bubbleSort()}>Sort</button>
</div>
<div className="slider-container">
1
<input type="range"
min="1"
max="100"
onChange={(e) => handleLength(e)}
className="slider"
id="myRange"
/>
100
</div>
{length}
</div>
);
}
export default Dummy2;
Instead of messing with the DOM I used state to keep track of changes so react will be charge of changing things.
When manipulating the array in the sorting function we need to remember that arr is part of state therefore it is immutable. any change that we do we need to do on a duplicate and apply the changes at the right time so an animation will occur, that is what I done in the createAnimation function.
To keep track on the colors I added to the state doneElements and compareElements. Every time an element get to it's final position it's index is added to doneElements. At any given time only two elements are compared therefore compareElements will contain only two elements or none.
I have a naive pattern matching function, and I'm trying to slow down execution of each comparison so I can create a visualiser for it. However, I want to be able to access my i and j variables outside of the function. I am attempting to do this by declaring them outside of the function, passing them in, and returning them after each match. This way I can press a button to control the flow of execution. However, they are not being returned properly, and I suspect this has something to do with my use of async/await, and the need to return the values as a Promise.
https://codesandbox.io/s/staging-http-0zm04?file=/src/App.tsx:0-1072
import React, { useState } from "react";
import "./styles.css";
const delay = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));
export const naive = async (text: string, pattern: string, i: number, j: number) => {
const matches = [];
let n = text.length;
let m = pattern.length;
while (i < n){
while (j < pattern.length && pattern[j] === text[i + j]){
j += 1;
await delay(500);
}
if (j === m){
matches.push(i)
}
return [i, j, matches]
}
}
export default function App() {
const [text, setText] = useState<string>("abcdefghijklmnopqrstuvwxyzabcd")
const [pat, setPat] = useState<string>("abc")
const [i, updateI] = useState(0);
const [j, updateJ] = useState(0);
const nextMatch = () => {
let results = naive(text, pat, i, j);
updateI(results[0]);
updateJ(results[1]);
}
return (
<div>
<button style = {{width: "100px", height: "50px"}}onClick = {() => nextMatch()}/>
{i}
{j}
</div>
);
}
As navie is an async function you have to add then.This would help to return correct i and j values
const nextMatch = () => {
naive(text, pat, i, j).then((results) => {
updateI(results[0]);
updateJ(results[1]);
});
};