React Hooks not changing object state until I scroll the page - javascript

I'm new with React, and I'm having a problem rendering a component ("setBoard" don't take effect until I scroll the page):
function GameManager() {
const [board, setBoard] = useState(new Board)
const handleKeyDown = useCallback(event => {
if (board.hasWon()) {
return;
}
if (event.keyCode >= 37 && event.keyCode <= 40) {
event.preventDefault();
var direction = event.keyCode - 37;
setBoard(board.move(direction))
}
}, [])
useEffect(() => {
window.addEventListener('keydown', handleKeyDown)
return() => {window.removeEventListener('keydown', handleKeyDown)}
}, [handleKeyDown])
return (
// <div onTouchStart={this.handleTouchStart.bind(this)} onTouchEnd={this.handleTouchEnd.bind(this)}>
<div>
<div class="heading" >
<button class="restart-button" onClick={restartGame} onTouchEnd={restartGame}>New Game</button>
< Score score={board.score} />
</div>
<BoardView board={board} />
</div>
);
}
My component was not re rendering, but the values of object "board" were changing:
return( </div> <BoardView board={board} /> </div>)
After some research, I found that React compares if the object has changed with Object.is, and the response is true, maybe because how I coded "board":
Board.prototype.move = function (direction) {
// 0 -> left, 1 -> up, 2 -> right, 3 -> down
this.clearOldTiles();
for (var i = 0; i < direction; ++i) {
this.cells = rotateLeft(this.cells);
}
var hasChanged = this.moveLeft();
for (var i = direction; i < 4; ++i) {
this.cells = rotateLeft(this.cells);
}
if (hasChanged) {
this.addRandomTile();
}
this.setPositions();
return this;
};
Maybe I should return something different than "return this" in .move()??
Any ideas??? (:
Thanks for reading!
EDIT:
Now is working, but I'm not sure if it was the best approach:
Changed the props in the component, instead of all the object:
<BoardView boardTiles={board.tiles} boardCells={board.cells} />
Now, it should update when board.tiles or board.cells changes.
Also change the way I set the object state (vars separately)
board.move(direction)
setBoard(prevBoard => ({...prevBoard, tiles: board.tiles, cells: board.cells, score: board.score}))
Is this way efficient? can be optimized?
Thanks!

Related

setTimeout in object method continues to run after object is destroyed

I have a class called Bullet which is essentially a div on a space invader webpage. When this bullet gets 'fired' I call a method which gradually moves the 'bullet' up the screen.
When the bullet gets to the edge of the screen I want to remove the whole bullet object from memory. However, the setTimeout loop continues to run even after I've deleted it (I think).
I'm sure there is a better way to do this! Perhaps it's foolish to run the loop like this?
TIA
this.bulletmove = new CustomEvent("bulletmove",{detail:this.name});
...
/**
* moves the bullet up the screen gradually
*/
fire(){
var that = this;
setTimeout(function(){
that.moveUp();
window.dispatchEvent(that.bulletmove);
that.fire();
},50);
}
The event is picked up in a controller script which checks if the bullet has reached the edge of the screen at which point it is deleted:
window.addEventListener('bulletmove', function(evt) {
checkCollision(evt);
},false);
...
/**
*Check if the bullet has gone off screen and deletes it
**/
function checkCollision(e){
var bulletName = e.detail;
var bullet = bullets[bulletName];
//check if the bullet has gone off screen
if (bullet.bottom < 0){
bullet.destroy;
delete bullets[e.detail];
bullet=null;
}
}
Have you tried a clearTimeout method to stop the setTimeout from firing?
https://www.freecodecamp.org/news/javascript-settimeout-how-to-set-a-timer-in-javascript-or-sleep-for-n-seconds/
const fireBullet = setTimeout(function(){
that.moveUp();
window.dispatchEvent(that.bulletmove);
that.fire();
},50);
clearTimeout(fireBullet)
I think you should use setInterval instead of calling fire() again - by calling that function, a new setTimeout is created (with a new handler); before the removal of the object, you call obj.halt(), and that clears the setInterval correctly.
const obj = {
name: "objName",
bulletmove() {
return new CustomEvent("bulletmove", {
detail: this.name
})
},
halt() {
clearInterval(this.intervalHandler)
},
intervalHandler: null,
fire() {
const handler = setInterval(() => {
// this.moveUp()
// console.log("move up")
window.dispatchEvent(this.bulletmove())
// this.fire()
}, 500)
this.intervalHandler = handler
},
}
let i = 0
window.addEventListener('bulletmove', function(e) {
// this if-else if mocks the collision detection
// expected: log obj.name 5 times, then clear the interval,
// then event should not be called anymore
if (i < 5) {
console.log(i, e.detail)
} else if (i < 8) {
obj.halt()
console.log(i)
} else if (i < 100) {
console.log(i, e.detail)
}
i++
})
obj.fire()
ANOTHER WAY
A cleaner approach would be if the fire method returned its own "clear function", and you could use that in the event handling:
const obj = {
name: "objName",
bulletmove() {
return new CustomEvent("bulletmove", {
detail: this.name
})
},
fire() {
const handler = setInterval(() => {
// this.moveUp()
// console.log("move up")
window.dispatchEvent(this.bulletmove())
// this.fire()
}, 500)
return () => clearInterval(handler)
},
}
let i = 0
const fireHandler = obj.fire()
const eventHandler = (clearFn) => (e) => {
// this if-else if mocks the collision detection
// expected: log obj.name 5 times, then clear the interval,
// then event should not be called anymore
if (i < 5) {
console.log(i, e.detail)
} else if (i < 8) {
clearFn()
console.log(i)
} else if (i < 100) {
console.log(i, e.detail)
}
i++
}
const eventHandlerWithRemoveFn = eventHandler(fireHandler)
window.addEventListener('bulletmove', eventHandlerWithRemoveFn)
The drawback of this method is that you need to add each object's event handler separately to the window, its benefit is more control, cleaner code (no need to save that handler in the object).
A MODIFIED VERSION FOR MULTIPLE INTERVALS
This is a version of the previous solution, where the clearing functions are stored in the window object:
const eventHandler = (e) => {
const i = e.detail.eventCounter
if (i < 3) {
console.log(i, e.detail.name)
} else if (i < 4) {
window.bulletIntervals[e.detail.name]()
console.log(i, e.detail.name + " is halted")
} else if (i < 100) {
console.log(i, e.detail.name)
}
}
const getBullet = (i) => ({
eventCounter: i, // only for mocking!
name: `objName-${i}`,
bulletmove() {
return new CustomEvent("bulletmove", {
detail: {
name: this.name,
eventCounter: this.eventCounter,
}
})
},
fire() {
const handler = setInterval(() => {
window.dispatchEvent(this.bulletmove())
this.eventCounter++
}, 500)
if (!window.bulletIntervals) window.bulletIntervals = {}
window.bulletIntervals[this.name] = () => clearInterval(handler)
},
})
const bullets = [
getBullet(0),
getBullet(1),
getBullet(2),
]
const fireAll = (bullets) => {
window.addEventListener("bulletmove", eventHandler)
bullets.forEach((bullet) => {
bullet.fire()
})
}
fireAll(bullets)
I would use RxJS to monitor the progress of your bullets.
In the example below I have three different bullets. Each within its own boundary. Once fired, they will immediately stop when they exit their box.
For each bullet we have an "animation frame" observable that emits when such a frame is made available by the browser (internally RxJS uses requestAnimationFrame for this). At that point we check whether the bullet is still within its parent bounding box. If it is we move it otherwise we don't and the subscription to the animation frame stream automatically ends.
const rightPos = el => el.getBoundingClientRect().right;
const moveBullet = (sel, pos) =>
document.querySelector(sel)
.style.left = `${pos}px`;
const fire = (bullet) => {
const el = document.querySelector(bullet);
const parentPos = rightPos(el.parentNode);
return animationFrames().pipe(
map(() => rightPos(el)),
takeWhile(pos => pos < parentPos)
);
}
const bullet1$ = fire('#bullet1');
const bullet2$ = fire('#bullet2');
const bullet3$ = fire('#bullet3');
const fire$ = fromEvent(document.querySelector('button'),'click');
fire$.subscribe(() => {
bullet1$.subscribe(pos => moveBullet('#bullet1', pos+1));
bullet2$.subscribe(pos => moveBullet('#bullet2', pos+1));
bullet3$.subscribe(pos => moveBullet('#bullet3', pos+1));
});
div {
height: 30px;
border: 1px solid black;
margin: 5px;
position: relative;
}
span { position: absolute; }
<script src="https://unpkg.com/rxjs#7.5.7/dist/bundles/rxjs.umd.min.js"></script>
<script>
const {animationFrames, fromEvent} = rxjs;
const {map, takeWhile} = rxjs.operators;
</script>
<div style="width:150px"><span id="bullet1">🏉</span></div>
<div style="width:300px"><span id="bullet2">🥏</span></div>
<div style="width:450px"><span id="bullet3">⚽️</span></div>
<button>Fire!</button>
In the following code, an outer div forms the boundaries of a playfield and all game elements inside the playfield are represented by divs.
Game state consists of an array of game elements (in this instance: one ship and zero or more bullets). Once an element is no longer visible (here: simply off the right-hand side of the playfield), it is removed from game state.
The game loop uses requestAnimationFrame to repeatedly render the game state to the playfield.
Bullet position is calculated using the time of firing and the time elapsed (I added a little randomness to bullet velocity just for fun).
Game elements such as bullets have an associated generator function called as part of the game loop, to retrieve the next state of the element (a bullet "moves by itself" after the initial appearance).
Firing a bullet in this design is as simple as creating a new bullet object with an initial position and an instance of a generator function to account for its trajectory; and then adding that pair to the game state.
const elem = ({ kind = 'div', classN = '' }) => {
const el = document.createElement(kind)
el.classList.add(classN)
return el
}
const applyStyle = (el, style) =>
(Object.entries(style)
.forEach(([k, v]) => el.style[k] = v), el)
const cssPixels = (str) => +(str.slice(0, -2))
const isVisible = (left) =>
cssPixels(left) < cssPixels(playfield.style.width)
const createPlayfield = () =>
applyStyle(elem({ classN: 'playfield' }), { width: '300px' })
const createShip = (startLeft, width) =>
[{ classN: 'ship', style: { left: startLeft, width } }, null]
const createBullet = (startLeft) => {
const b = {
classN: 'bullet',
style: { left: startLeft },
firingTime: +new Date(),
velocity: 0.5,
velocitySeed: Number('1.' + ~~(Math.random() * 9)),
startLeft
}
const g = bulletStateGen(b)
return [ b, () => g.next() ]
}
const bulletPos = ({ firingTime,
startLeft,
velocity,
velocitySeed }, now = +new Date()) =>
`${~~(velocity * (now - firingTime) * velocitySeed + cssPixels(startLeft))}px`
const bulletStateGen = function*(b) {
while (1) {
const left = bulletPos(b)
if (!isVisible(left))
break
b.style = { left }
yield(b)
}
}
const fire = (startLeft) =>
state.unshift(createBullet(startLeft))
const tick = () =>
state = state.reduce((acc, [o, next]) => {
if (!next)
return acc.push([o, next]), acc
const { value, done } = next()
if (done)
return acc
return acc.push([value, next]), acc
}, [])
const blank = () => playfield.innerHTML = ''
const render = () => {
blank()
state.forEach(([{ classN, style = {} }]) =>
playfield.appendChild(applyStyle(elem({ classN }), style)))
}
let ship = createShip('10px', '50px')
let state = [ship]
let playfield = createPlayfield()
const gameLoop = () =>
(render(), tick(), requestAnimationFrame(gameLoop))
const init = () => {
document.body.appendChild(playfield)
document.body.onkeyup = (e) =>
e.key === " "
&& fire(`${cssPixels(ship[0].style.left) + cssPixels(ship[0].style.width)}px`)
}
init()
gameLoop(state, playfield)
.playfield {
height: 300px;
background-color: black;
position: relative;
}
.ship {
top: 138px;
height: 50px;
background-color: gold;
position: absolute;
border-radius: 7px 22px 22px 7px;
}
.bullet {
top: 163px;
width: 10px;
height: 2px;
background-color: silver;
position: absolute;
}
Click on the game to focus it, and then press spacebar to fire!

Automatically trigger an OnClick event of a specific component in React

I'm a beginner to React and Javascript and I'm attempting to create Minesweeper in React from scratch. I've run into an issue where I want to replicate the functionality of Minesweeper where if you click a tile with zero mines around it, the surrounding tiles will automatically reveal themselves. However, I'm having trouble figuring out how to do that. My instinct is to grab the surrounding tiles by an id, and somehow manually trigger the OnClick event, but as far as I'm aware my Tile component has no knowledge of other Tile components and thus no way of accessing them. And I don't see how I could do it from my Board component. Any advice would be greatly appreciated!
Board Component:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
height: 8,
width: 8,
num_mines: 10
}
game_logic.initalizeGame(8,8,10);
game_logic.createGame();
}
render() {
var all_tiles = [];
for (var i = 0; i < this.state.height; i++) {
var row_tiles = [];
for (var j = 0; j < this.state.width; j++) {
row_tiles.push(<Tile key={'Tile ' + (i*this.state.height + j)} value={game_logic.tile_values[i][j]} />)
}
all_tiles[i] = row_tiles;
}
return (
<div>
{all_tiles.map((value, index) => {
return <div key={'Row ' + index}>{value}</div>
})}
</div>
)
}
}
export default Board;
Tile Component:
class Tile extends React.Component {
constructor(props) {
super(props);
this.state = {
imgSrc: null,
imgAlt: '',
value: '',
};
}
tileClick = (e) => {
//left click: unveil tile
if (e.type === "click" && this.state.imgAlt === '') {
if (this.props.value < 0) {
this.setState({value: '', imgSrc: mine, imgAlt: "mine"});
}
else {
this.setState({value: this.props.value})
if (this.props.value === 0) {
//automatically left click all 8 surrounding tiles
}
}
}
//right click: mark or unmark tile
else if (e.type === "contextmenu") {
e.preventDefault();
if (this.state.value === '' && this.state.imgAlt !== "mine") {
if (this.state.imgAlt !== '') {
this.setState({imgSrc: null, imgAlt: ''});
}
else {
this.setState({imgSrc: flag, imgAlt: "flag"});
}
}
}
}
render() {
return (
<button
className="tile"
onClick={this.tileClick} onContextMenu={this.tileClick}>
<img src={this.state.imgSrc} alt={this.state.imgAlt} />
{this.state.value}
</button>
);
}
}
export default Tile;
You need the child component to be able to see and do something with the parent's game_logic.tile_values. While it'd be possible to pass the values down from the parent and try to do something with them there, it'd be easier to put the click handlers in the parent so everything can be accessed from there easily.
You'll also need to change around the way you handle state. Having each individual component have its own state will make cross-component communication very difficult, but that's what you need in order for the change in one component (tile) to affect other tiles. Put the state of whether a tile's been revealed or not in the parent.
class Board extends React.Component {
constructor(props) {
super(props);
game_logic.initalizeGame(8,8,10);
game_logic.createGame();
this.state = {
height: 8,
width: 8,
num_mines: 10,
revealed: Array.from(
{ length: 8 },
() => new Array(8).fill(false)
);
}
}
handleReveal = (x, y, visited = new Set()) => {
const tileKey = x + '_' + y;
// if out of bounds, or if visited before, don't do anything
if (visited.has(tileKey) || game_logic.tile_values[x]?.[y] === undefined) return;
visited.add(tileKey);
const isZero = game_logic.tile_values[x][y] === 0;
this.setState({
...this.state,
revealed: this.state.map(
(row, i) => i !== x
? row
: row.map(
(tileState, j) => y === j ? true : tileState
)
)
});
if (isZero) {
handleReveal(x - 1, y);
handleReveal(x + 1, y);
handleReveal(x, y - 1);
handleReveal(x, y + 1);
}
};
Now that the parent component has handleReveal, pass it down to each tile. When there's a left click, call the passed down handleReveal, and have the children tiles decide what image to render by checking whether the parent's revealed state for that X and Y is true or false.
The Tiles probably shouldn't have state themselves except for whether they've been flagged or not. The imgSrc can be determined directly from the props, as can the value. Better not to duplicate state over both parents and children - doing it that way makes things messy and hard to deal with, especially when it comes to updates.
Hello You have to update line
{this.state.value}
with the below
{this.state.value}

Can't fix carousel algorithm in react

I have a carousel of slides in my react.js project without using any libraries. When I use an odd amount of images everything works. But when I use even amount, although currentIndex is changing properly only odd images are displayed like 1,3,5 in this example with six images. Can anyone spot what is wrong with my code so it would work with ane amount of images not only with odd ones? Thanks very much
import React from 'react';
import Slide from './Slide';
import img1 from "../assets/img1.jpg";
import img2 from "../assets/img2.jpg";
import img3 from "../assets/img3.jpg";
import img4 from "../assets/img4.jpg";
import img5 from "../assets/img5.jpg";
import img6 from "../assets/img6.jpg";
class Test extends React.Component {
state = {
currentIndex: 0,
images: [img1, img2, img3, img4, img5, img6]
}
prevSlide = () => {
const lastIndex = this.state.images.length - 1;
const resetIndex = this.state.currentIndex === 0;
const index = resetIndex ? lastIndex : this.state.currentIndex - 1;
this.setState({
currentIndex: index
});
};
nextSlide = () => {
const lastIndex = this.state.images.length - 1;
const resetIndex = this.state.currentIndex === lastIndex;
const index = resetIndex ? 0 : this.state.currentIndex + 1;
this.setState({
currentIndex: index
});
};
render() {
const index = this.state.currentIndex;
let newImagesArray = this.state.images.slice(index, index + 6);
if (newImagesArray.length < 6) {
newImagesArray = newImagesArray.concat(
this.state.images.slice(0, 6 - newImagesArray.length)
);
}
return (
<div className="paint__container">
{newImagesArray.map((image, i) =>
this.state.currentIndex === i ? (
<Slide key={i} url={image} alt="" />
) : null
)}
<div className="left__arrow" onClick={this.prevSlide}></div>
<div className="right__arrow" onClick={this.nextSlide}></div>
</div>
);
}
}
export default Test;
okay, thank you for providing the full code, looking at the component on github
we can find
you have nextSlide defined twice, where the second I guess will overwrite the first declaration
while you have the currentIndex in state why you are searching for the target slide in your render function? you don't have to do this my friend, while currentIndex correctly calculate the index then you just render the slide at that index, that's why we are using react after all
render() {
const index = this.state.currentIndex;
const images = this.state.images;
return (
<div className="paint__container">
<Slide url={images[index]} alt="" />
<div className="left__arrow" onClick={this.prevSlide}></div>
<div className="right__arrow" onClick={this.nextSlide}></div>
</div>
);
}

How to correctly wait on state to update/render instead of using a delay/timeout function?

I will attempt to keep this brief, but I am not 100% sure of the correct method of achieving what I am aiming for. I have been thrown in the deep end with React with not much training, so I have most likely been going about most of this component incorrectly, a point in the right direction will definitely help, I don't really expect for someone to completely redo my component for me as it's quite long.
I have a navigation bar SubNav, that finds the currently active item based upon the url/path, this will then move an underline element that inherits the width of the active element. To do this, I find the position of the active item and position accordingly. The same goes for when a user hovers over another navigation item, or when the window resizes it adjusts the position accordingly.
I also have it when at lower resolutions, when the nav gets cut off to have arrows appear to scroll left/right on the navigation to view all navigation items.
Also, if on a lower resolution and the currently active navigation item is off screen, the navigation will scroll to that item and then position the underline correctly.
This, currently works as I have it in my component, this issue is, I don't believe I have done this correctly, I am using a lodash function delay to delay at certain points (I guess to get the correct position of certain navigation items, as it isn't correct at the time of the functions call), which I feel is not the way to go. This is all based on how fast the page loads etc and will not be the same for each user.
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
400,
setArrowStyle(styling)
);
Without using the delay, the values coming back from my state are incorrect as they haven't been set yet.
My question is, how do I go about this correctly? I know my code below is a bit of a read but I have provided a CODESANBOX to play about with.
I have 3 main functions, that all sort of rely on one another:
getPostion()
This function finds the active navigation item, checks if it's within the viewport, if it is not, then it changes the left position of the navigation so it's the leftmost navigation item on the screen, and via setSizes(getSizes()) moves the underline directly underneath.
getSizes()
This is called as an argument within setSizes to update the sizes state, which returns the left and right boundaries of all navigation items
getUnderlineStyle()
This is called as an argument within setUnderLineStyle within the getSizes() function to update the position of the underline object in relation to the position of active navigation item grabbed from the sizes state, but I have to pass the sizesObj as an argument in setSizes as the state has not been set. I think this is where my confusion began, I think I was under the impression, that when I set the state, I could then access it. So, I started using delay to combat.
Below is my whole Component, but can be seen working in CODESANBOX
import React, { useEffect, useState, useRef } from "react";
import _ from "lodash";
import { Link, Route } from "react-router-dom";
import "../../scss/partials/_subnav.scss";
const SubNav = props => {
const subNavLinks = [
{
section: "Link One",
path: "link1"
},
{
section: "Link Two",
path: "link2"
},
{
section: "Link Three",
path: "link3"
},
{
section: "Link Four",
path: "link4"
},
{
section: "Link Five",
path: "link5"
},
{
section: "Link Six",
path: "link6"
},
{
section: "Link Seven",
path: "link7"
},
{
section: "Link Eight",
path: "link8"
}
];
const currentPath =
props.location.pathname === "/"
? "link1"
: props.location.pathname.replace(/\//g, "");
const [useArrows, setUseArrows] = useState(false);
const [rightArrow, updateRightArrow] = useState(false);
const [leftArrow, updateLeftArrow] = useState(false);
const [sizes, setSizes] = useState({});
const [underLineStyle, setUnderLineStyle] = useState({});
const [arrowStyle, setArrowStyle] = useState({});
const [activePath, setActivePath] = useState(currentPath);
const subNavRef = useRef("");
const subNavListRef = useRef("");
const arrowRightRef = useRef("");
const arrowLeftRef = useRef("");
let elsRef = Array.from({ length: subNavLinks.length }, () => useRef(null));
useEffect(
() => {
const reposition = getPosition();
subNavArrows(window.innerWidth);
if (!reposition) {
setSizes(getSizes());
}
window.addEventListener(
"resize",
_.debounce(() => subNavArrows(window.innerWidth))
);
window.addEventListener("resize", () => setSizes(getSizes()));
},
[props]
);
const getPosition = () => {
const activeItem = findActiveItem();
const itemHidden = findItemInView(activeItem);
if (itemHidden) {
const activeItemBounds = elsRef[
activeItem
].current.getBoundingClientRect();
const currentPos = subNavListRef.current.getBoundingClientRect().left;
const arrowWidth =
arrowLeftRef.current !== "" && arrowLeftRef.current !== null
? arrowLeftRef.current.getBoundingClientRect().width
: arrowRightRef.current !== "" && arrowRightRef.current !== null
? arrowRightRef.current.getBoundingClientRect().width
: 30;
const activeItemPos =
activeItemBounds.left * -1 + arrowWidth + currentPos;
const styling = {
left: `${activeItemPos}px`
};
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
400,
setArrowStyle(styling)
);
return true;
}
return false;
};
const findActiveItem = () => {
let activeItem;
subNavLinks.map((i, index) => {
const pathname = i.path;
if (pathname === currentPath) {
activeItem = index;
return true;
}
return false;
});
return activeItem;
};
const getSizes = () => {
const rootBounds = subNavRef.current.getBoundingClientRect();
const sizesObj = {};
Object.keys(elsRef).forEach(key => {
const item = subNavLinks[key].path;
const el = elsRef[key];
const bounds = el.current.getBoundingClientRect();
const left = bounds.left - rootBounds.left;
const right = rootBounds.right - bounds.right;
sizesObj[item] = { left, right };
});
setUnderLineStyle(getUnderlineStyle(sizesObj));
return sizesObj;
};
const getUnderlineStyle = (sizesObj, active) => {
sizesObj = sizesObj.length === 0 ? sizes : sizesObj;
active = active ? active : currentPath;
if (active == null || Object.keys(sizesObj).length === 0) {
return { left: "0", right: "100%" };
}
const size = sizesObj[active];
const styling = {
left: `${size.left}px`,
right: `${size.right}px`,
transition: `left 300ms, right 300ms`
};
return styling;
};
const subNavArrows = windowWidth => {
let totalSize = sizeOfList();
_.delay(
() => {
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
300,
setUseArrows(totalSize > windowWidth)
);
};
const sizeOfList = () => {
let totalSize = 0;
Object.keys(elsRef).forEach(key => {
const el = elsRef[key];
const bounds = el.current.getBoundingClientRect();
const width = bounds.width;
totalSize = totalSize + width;
});
return totalSize;
};
const onHover = active => {
setUnderLineStyle(getUnderlineStyle(sizes, active));
setActivePath(active);
};
const onHoverEnd = () => {
setUnderLineStyle(getUnderlineStyle(sizes, currentPath));
setActivePath(currentPath);
};
const scrollRight = () => {
const currentPos = subNavListRef.current.getBoundingClientRect().left;
const arrowWidth = arrowRightRef.current.getBoundingClientRect().width;
const subNavOffsetWidth = subNavRef.current.clientWidth;
let nextElPos;
for (let i = 0; i < elsRef.length; i++) {
const bounds = elsRef[i].current.getBoundingClientRect();
if (bounds.right > subNavOffsetWidth) {
nextElPos = bounds.left * -1 + arrowWidth + currentPos;
break;
}
}
const styling = {
left: `${nextElPos}px`
};
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
500,
setArrowStyle(styling)
);
};
const scrollLeft = () => {
const windowWidth = window.innerWidth;
// const lastItemInView = findLastItemInView();
const firstItemInView = findFirstItemInView();
let totalWidth = 0;
const hiddenEls = elsRef
.slice(0)
.reverse()
.filter((el, index) => {
const actualPos = elsRef.length - 1 - index;
if (actualPos >= firstItemInView) return false;
const elWidth = el.current.getBoundingClientRect().width;
const combinedWidth = elWidth + totalWidth;
if (combinedWidth > windowWidth) return false;
totalWidth = combinedWidth;
return true;
});
const targetEl = hiddenEls[hiddenEls.length - 1];
const currentPos = subNavListRef.current.getBoundingClientRect().left;
const arrowWidth = arrowLeftRef.current.getBoundingClientRect().width;
const isFirstEl =
targetEl.current.getBoundingClientRect().left * -1 + currentPos === 0;
const targetElPos = isFirstEl
? targetEl.current.getBoundingClientRect().left * -1 + currentPos
: targetEl.current.getBoundingClientRect().left * -1 +
arrowWidth +
currentPos;
const styling = {
left: `${targetElPos}px`
};
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
500,
setArrowStyle(styling)
);
};
const findItemInView = pos => {
const rect = elsRef[pos].current.getBoundingClientRect();
return !(
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);
};
const findLastItemInView = () => {
let lastItem;
for (let i = 0; i < elsRef.length; i++) {
const isInView = !findItemInView(i);
if (isInView) {
lastItem = i;
}
}
return lastItem;
};
const findFirstItemInView = () => {
let firstItemInView;
for (let i = 0; i < elsRef.length; i++) {
const isInView = !findItemInView(i);
if (isInView) {
firstItemInView = i;
break;
}
}
return firstItemInView;
};
return (
<div
className={"SubNav" + (useArrows ? " SubNav--scroll" : "")}
ref={subNavRef}
>
<div className="SubNav-content">
<div className="SubNav-menu">
<nav className="SubNav-nav" role="navigation">
<ul ref={subNavListRef} style={arrowStyle}>
{subNavLinks.map((el, i) => (
<Route
key={i}
path="/:section?"
render={() => (
<li
ref={elsRef[i]}
onMouseEnter={() => onHover(el.path)}
onMouseLeave={() => onHoverEnd()}
>
<Link
className={
activePath === el.path
? "SubNav-item SubNav-itemActive"
: "SubNav-item"
}
to={"/" + el.path}
>
{el.section}
</Link>
</li>
)}
/>
))}
</ul>
</nav>
</div>
<div
key={"SubNav-underline"}
className="SubNav-underline"
style={underLineStyle}
/>
</div>
{leftArrow ? (
<div
className="SubNav-arrowLeft"
ref={arrowLeftRef}
onClick={scrollLeft}
/>
) : null}
{rightArrow ? (
<div
className="SubNav-arrowRight"
ref={arrowRightRef}
onClick={scrollRight}
/>
) : null}
</div>
);
};
export default SubNav;
You can make use of useLayoutEffect hook to determine whether the values have been updated and take an action. Since you want to determine whether all the values has been updated, you need to compare old and new values in useEffect. You can refer to the below post to know how to write a usePrevious custom hook
How to compare oldValues and newValues on React Hooks useEffect?
const oldData = usePrevious({ rightArrow, leftArrow, sizes});
useLayoutEffect(() => {
const {rightArrow: oldRightArrow, leftArrow: oldLeftArrow, sizes: oldSizes } = oldData;
if(oldRightArrow !== rightArrow && oldLeftArrow !== leftArrow and oldSizes !== sizes) {
setArrowStyle(styling)
}
}, [rightArrow, leftArrow, sizes])
I think the reason of your delay is necessary here since you calculate based on rectangles of the first and the last element which are affected when you click on button and do animation of scrolling 500ms. So as a result your calculation needs to wait for animation to be done. change the number of animation and delay you will see the relation.
the style I meant.
#include transition(all 500ms ease);
In short, I think what you are using is the right way as long as you have animations related to the calculation.
setState takes an optional second argument which is a callback that executes after the state has been updated and the component has been re-rendered.
Another option is the componentDidUpdate lifecycle method.

Warning: setState(...): Can only update a mounted or mounting component. This usually means

I am working on an exercise in the Udemy Advanced Webdeveloper Bootcamp. The exercise asked to come up with a page of 32 boxes that randomly change colour (every x seconds). My solution is not exactly that. I change the color of all 32 boxes at the same time. It almost works. I get random 32 boxes initially, but does not change the color later. My console tells me I am doing something wrong with the setState. But I cannot figure out what. I think my changeColor is a pure function:
import React, { Component } from 'react';
import './App.css';
class Box extends Component {
render() {
var divStyle = {
backgroundColor: this.props.color
}
return(
<div className="box" style={divStyle}></div>
);
}
}
class BoxRow extends Component {
render() {
const numOfBoxesInRow = 8;
const boxes = [];
for(var i=0; i < numOfBoxesInRow; i++) {
boxes.push(<Box color={this.props.colors[i]} key={i+1}/>);
}
return(
<div className="boxesWrapper">
{boxes}
</div>
);
}
}
class BoxTable extends Component {
constructor(props) {
super(props);
this.getRandom = this.getRandom.bind(this);
this.changeColors = this.changeColors.bind(this);
this.state = {
randomColors: this.getRandom(this.props.allColors, 32) // hardcoding
};
this.changeColors();
}
changeColors() {
setInterval(
this.setState({randomColors: this.getRandom(this.props.allColors, 32)}), 5000);
}
getRandom(arr, n) {
var result = new Array(n),
len = arr.length,
taken = new Array(len);
if (n > len)
throw new RangeError("getRandom: more elements taken than available");
while (n--) {
var x = Math.floor(Math.random() * len);
result[n] = arr[x in taken ? taken[x] : x];
taken[x] = --len in taken ? taken[len] : len;
}
return result;
}
render () {
const numOfRows = 4;
const rows = [];
for(let i=0; i < numOfRows; i++) {
rows.push(
<BoxRow colors={this.state.randomColors.slice(8*i,8*(1+i))} key={i+1}/>
)
}
return (
<div className="rowsWrapper">
{rows}
</div>
);
}
}
BoxTable.defaultProps = {
allColors: ["AliceBlue","AntiqueWhite","Aqua","Aquamarine","Azure","Beige",
"Bisque","Black","BlanchedAlmond","Blue","BlueViolet","Brown","BurlyWood",
"CadetBlue","Chartreuse","Chocolate","Coral","CornflowerBlue","Cornsilk",
"Crimson","Cyan","DarkBlue","DarkCyan","DarkGoldenRod","DarkGray","DarkGrey",
"DarkGreen","DarkKhaki","DarkMagenta","DarkOliveGreen","Darkorange",
"DarkOrchid","DarkRed","DarkSalmon","DarkSeaGreen","DarkSlateBlue",
"DarkSlateGray","DarkSlateGrey","DarkTurquoise","DarkViolet","DeepPink",
"DeepSkyBlue","DimGray","DimGrey","DodgerBlue","FireBrick","FloralWhite",
"ForestGreen","Fuchsia","Gainsboro","GhostWhite","Gold","GoldenRod","Gray",
"Grey","Green","GreenYellow","HoneyDew","HotPink","IndianRed","Indigo",
"Ivory","Khaki","Lavender","LavenderBlush","LawnGreen","LemonChiffon",
"LightBlue","LightCoral","LightCyan","LightGoldenRodYellow","LightGray",
"LightGrey","LightGreen","LightPink","LightSalmon","LightSeaGreen",
"LightSkyBlue","LightSlateGray","LightSlateGrey","LightSteelBlue",
"LightYellow","Lime","LimeGreen","Linen","Magenta","Maroon",
"MediumAquaMarine","MediumBlue","MediumOrchid","MediumPurple",
"MediumSeaGreen","MediumSlateBlue","MediumSpringGreen","MediumTurquoise",
"MediumVioletRed","MidnightBlue","MintCream","MistyRose","Moccasin",
"NavajoWhite","Navy","OldLace","Olive","OliveDrab","Orange","OrangeRed",
"Orchid","PaleGoldenRod","PaleGreen","PaleTurquoise","PaleVioletRed",
"PapayaWhip","PeachPuff","Peru","Pink","Plum","PowderBlue","Purple",
"Red","RosyBrown","RoyalBlue","SaddleBrown","Salmon","SandyBrown",
"SeaGreen","SeaShell","Sienna","Silver","SkyBlue","SlateBlue","SlateGray",
"SlateGrey","Snow","SpringGreen","SteelBlue","Tan","Teal","Thistle",
"Tomato","Turquoise","Violet","Wheat","White","WhiteSmoke","Yellow","YellowGreen"]
}
export default BoxTable
You are calling this.setState before the component has mounted (from the constructor). Try instead making your first this.ChangeColors from the componentDidMount lifecycle function.
Additionally, it's not a bad idea to clear the interval when it unmounts in componentWillUnMount
Edit: By changing the interval to call after the first wait you do prevent the error, for now. I'd recommend using the lifecycle functions to build good habits. It's just a temporary assignment, but in a full project you'd be putting yourself at risk to break the component again later by making it possible to call this.setState before it is reliably mounted.
You need to use a lambda function in order to use setState inside setInterval
setInterval(() => {
this.setState({randomColors: this.getRandom(this.props.allColors,
32)});
}, 5000)
try to change your changeColors function to like this:
changeColors() {
setInterval(() => this.setState({randomColors: this.getRandom(this.props.allColors, 32)}), 5000);
}
the first param of setInterval is function, in your original code you already executed this setState and didn't passed the function itself
You will need to update the state after the component creation phase, inside componentDidMount()
class BoxTable extends Component {
constructor(props) {
super(props);
this.getRandom = this.getRandom.bind(this);
this.changeColors = this.changeColors.bind(this);
this.state = {
randomColors: this.getRandom(this.props.allColors, 32) // hardcoding
};
// delete this line
//this.changeColors();
}
// replace changeColors by componentDidMount,
// this function will be called automatically by react
componentDidMount() {
setInterval(
this.setState({randomColors: this.getRandom(this.props.allColors, 32)}), 5000);
}
getRandom(arr, n) {
var result = new Array(n),
len = arr.length,
taken = new Array(len);
if (n > len)
throw new RangeError("getRandom: more elements taken than available");
while (n--) {
var x = Math.floor(Math.random() * len);
result[n] = arr[x in taken ? taken[x] : x];
taken[x] = --len in taken ? taken[len] : len;
}
return result;
}
render () {
const numOfRows = 4;
const rows = [];
for(let i=0; i < numOfRows; i++) {
rows.push(
<BoxRow colors={this.state.randomColors.slice(8*i,8*(1+i))} key={i+1}/>
)
}
return (
<div className="rowsWrapper">
{rows}
</div>
);
}
}

Categories

Resources