Run code after state update - reactjs

I have this function, it's registered on click.
mobileZoomClick () {
const elem = this.state.scale.carousel
if (this.state.zoom && elem.scale < 1.1) {
this.zoomOut()
}
}
<div ref='carousel'
onClick={this.mobileZoomClick.bind(this)}
className='carousel' style={this.fullscreen('carousel')}
test={this.state.carouselTransition === 'transitionEnd' && !this.state.zoom && this.flickity()}
>
I would like to add this.flickity() into the function:
mobileZoomClick () {
const elem = this.state.scale.carousel
if (this.state.zoom && elem.scale < 1.1) {
this.zoomOut()
}
if(this.state.carouselTransition === 'transitionEnd' && !this.state.zoom){
this.flickity()
}
}
but I need it to be updated on state-change. The current solution technically works but throws an error.
this is how the new states are set (this first one is for checking if certain elements are in a css transition:
transitionState (target) {
// den här behöver fallback för inte transition
const done = (e) => {
this.setState({[stateName]: 'transitionEnd'}, () => {
this.setState({[stateName]: 'transitionComplete'})
})
e.target.removeEventListener(e.type, done)
}
const elem = this.refs[target]
const stateName = target + 'Transition'
this.setState({[stateName]: 'transitionStarted'})
elem.addEventListener('transitionend', done)
}
this is for zooming out:
zoomOut () { this.setState({zoom: false}) }

Related

React update state of a button based on another button

For this project I am currently working on, I need to highlight the button that was clicked on each layer/row. However, the way I have right now it highlights every button that was clicked.
I need something like this:
correct highlighted path
But then when I click on the same row, it does not remove the highlight from the button that I pressed before. How can I update and reset the state of the previous button that was clicked? I tried to use the useRef hook for this but I haven't been successful so far.
wrong highlighted path
EDIT: Added code
This is the code that I have for the component of each row in the website:
function StackRow({ data, partition, level, index, onClick, getInfo, isApp }) {
const classes = useStyles({ level: level });
const rowRef = useRef();
const handleSelectedButtons = (flag, setFlag, btnRef) => {
console.log(rowRef);
};
return (
<Card
key={partition + '_' + index + '_' + level}
className={classes.card}
id={level}
ref={rowRef}
>
{data.map((field) => {
return (
<StackItem
key={partition + '_' + field[0] + '_' + level}
data={field[0]}
info={field[1]}
level={level}
onClick={onClick}
getInfo={getInfo}
isApp={isApp}
handleSelectedButtons={handleSelectedButtons}
rowRef={rowRef}
/>
);
})}
</Card>
);
}
And this is the code I have for each button of the row:
function StackItem({
data,
info,
level,
onClick,
getInfo,
isApp,
handleSelectedButtons,
}) {
const [flag, setFlag] = useState(false);
const btnRef = useRef();
const styleProps = {
backgroundColor: flag ? '#06d6a0' : level % 2 === 0 ? '#22223b' : '#335c67',
};
const classes = useStyles(styleProps);
return (
<Button
ref={btnRef}
isselected={flag.toString()}
key={data}
className={classes.button}
variant="outlined"
onClick={(event) => {
onClick(event, setFlag, btnRef);
handleSelectedButtons(flag, setFlag, btnRef);
getInfo(info, level, isApp);
}}
disableElevation={true}
>
{data}
</Button>
);
}
There are some useless variables and states there because I have been trying all sort of stuff to do this.
EDIT: Added data sample & project structure
Data looks like:
{
application: {
cmake: {
info: str,
versions: {
version_no: {
application: {...}
}
}
},
gcc: {...},
git: {...},
intel: {...},
.
.
.
}
}
The structure of the project is like:
App
L Stack
L StackRow
L StackItem
Where App is the entire application, Stack is the container for everything in the images apart from the search box, StackRow matches one row of the Stack, and StackItem is one item/button from the StackRow.
EDIT: Added Stack component
function Stack({ data, partition, getInfo }) {
const [level, setLevel] = useState(0);
const [cards, setCards] = useState([]);
const [isApp, setIsApp] = useState(true);
const [selected, setSelected] = useState([]);
const [prevLevel, setPrevLevel] = useState(-1);
const cardsRef = useRef();
const handleClick = (event, setFlag, btnRef) => {
let rows = cardsRef.current.childNodes;
let currBtn = event.target.innerText;
let curr;
for (let i = 0; i < rows.length; i++) {
let rowItems = rows[i].childNodes;
for (let j = 0; j < rowItems.length; j++) {
if (currBtn === rowItems[j].textContent) {
curr = rowItems[j].parentElement;
}
}
}
let id;
for (let i = 0; i < rows.length; i++) {
if (curr.textContent === rows[i].textContent) {
id = i;
}
}
if (level === id) {
if (id % 2 === 0) {
setIsApp(true);
if (selected.length === 0) {
setSelected([...selected, data[currBtn].versions]);
} else {
let lastSelected = selected[selected.length - 1];
setSelected([...selected, lastSelected[currBtn].versions]);
}
} else {
let lastSelected = selected[selected.length - 1];
setSelected([...selected, lastSelected[currBtn].child]);
setIsApp(false);
}
setPrevLevel(level);
setLevel(level + 1);
} else {
let newSelected = selected.slice(0, id);
if (id % 2 === 0) {
setIsApp(true);
if (newSelected.length === 0) {
setSelected([...newSelected, data[currBtn].versions]);
} else {
let lastSelected = newSelected[newSelected.length - 1];
setSelected([...newSelected, lastSelected[currBtn].versions]);
}
} else {
let lastSelected = newSelected[newSelected.length - 1];
setSelected([...newSelected, lastSelected[currBtn].child]);
setIsApp(false);
}
setPrevLevel(level);
setLevel(id + 1);
}
setFlag(true);
};
useEffect(() => {
let fields = [];
let lastSelected = selected[selected.length - 1];
if (level % 2 !== 0) {
fields = Object.keys(lastSelected).map((key) => {
let path = lastSelected[key].path;
let module = lastSelected[key].module_name;
let info = 'module: ' + module + ' path: ' + path;
return [key, info];
});
} else {
if (selected.length !== 0)
fields = Object.keys(lastSelected).map((key) => {
let info = lastSelected[key].info;
return [key, info];
});
}
if (fields.length > 0) {
if (level > prevLevel) {
setCards((prevState) => [...prevState, fields]);
} else {
setCards((prevState) => [
...prevState.slice(0, selected.length),
fields,
]);
}
}
}, [selected, level, prevLevel]);
useEffect(() => {
let fields = Object.keys(data).map((key) => {
let info = data[key].info;
return [key, info];
});
setCards([fields]);
setLevel(0);
}, [data]);
useEffect(() => {
setLevel(0);
setPrevLevel(-1);
setSelected([]);
}, [partition]);
if (cards) {
return (
<div ref={cardsRef}>
{cards.map((card, index) => (
<StackRow
data={card}
partition={partition}
level={index}
index={cards.indexOf(card)}
onClick={handleClick}
getInfo={getInfo}
isApp={isApp}
/>
))}
</div>
);
} else return null;
}
EDIT: Added data sample
{
cmake: {
info: "A cross-platform, open-source build system. CMake is a family of tools designed to build, test and package software.",
versions: {
"3.17.3": {
child: {},
module_name: "cmake/3.17.3",
path: "/opt/apps/nfs/spack/var/spack/environments/matador/modules/linux-centos8-x86_64/Core/cmake/3.17.3.lua",
version_no: "3.17.3"
}
}
},
gcc: {
info: "...",
versions: {
"8.4.0": {
child: {
cmake: {...},
cuda: {...},
cudnn: {...},
openmpi: {...},
.
.
.
},
module_name: "...",
path: "...",
version_no: "..."
}
"9.3.0": {...},
"10.1.0": {...}
}
}
}

State variable hook does not increment within closure

codesandbox.io/s/github/Tmcerlean/battleship
I am developing a simple board game and need to increment a state variable when a player clicks on a cell with a valid move.
The functionality for validating the move and making the move is all in place, however, I am having difficulty updating the state within the event listener.
I can see that the state is being updated when observed from a useEffect hook, but not when viewed from within the function (even following successive calls).
I have done some reading and believe it could have something to do with having a stale closure, but I am not certain.
My approach to solve this issue was to remove and then re-add the click event listener following every click by the user.
My assumption was that this would cause the correct (newly incremented) state variable to be picked up. Unfortunately, this does not appear to be the case and within the event listener function, the variable is never incremented from 0.
I initialise the state variable here:
const [placedShips, setPlacedShips] = useState(0);
Next, a click event listener is applied to each cell within the gameboard:
const clickListener = (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(e.target.id);
let end = start + currentShip().length - 1;
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
playerGameboard.placeShip(placedShips, direction, start, end);
setPlacedShips((oldValue) => oldValue + 1);
console.log(placedShips);
}
};
const setEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.addEventListener("click", (e) => {
clickListener(e);
});
});
};
You will see that the setPlacedships state variable is incremented here and there is a console log to report its value.
I am aware that the useState hook is asynchronous and so console.log will show 0 for the first time it is called. Consequently, I have a useEffect hook deployed outside of the function which also contains a console.log to report the changed value of setPlacedShips:
useEffect(() => {
removeEventListeners();
setEventListeners();
console.log(placedShips)
}, [placedShips])
After every click the placedShips variable is incremented by 1 and then two functions are run:
const removeEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.removeEventListener("click", (e) => {
clickListener(e);
});
});
};
which is immediately followed by the original setEventListeners function:
const setEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.addEventListener("click", (e) => {
clickListener(e);
});
});
};
As mentioned above, the issue is that the console log within the setEventListeners function constantly remains at 0, while the console log within the useEffect hook increments as expected.
For reference, here is the full component I am working on currently:
import React, { useEffect, useState, useLayoutEffect } from "react";
import gameboardFactory from "../../factories/gameboardFactory";
import Table from "../Reusable/Table";
import "./GameboardSetup.css";
// -----------------------------------------------
//
// Desc: Gameboard setup phase of game
//
// -----------------------------------------------
let playerGameboard = gameboardFactory();
const GameboardSetup = () => {
const [humanSetupGrid, setHumanSetupGrid] = useState([]);
const [ships, _setShips] = useState([
{
name: "carrier",
length: 5,
direction: "horizontal",
},
{
name: "battleship",
length: 4,
direction: "horizontal",
},
{
name: "cruiser",
length: 3,
direction: "horizontal",
},
{
name: "submarine",
length: 3,
direction: "horizontal",
},
{
name: "destroyer",
length: 2,
direction: "horizontal",
},
]);
const [placedShips, setPlacedShips] = useState(0);
const createGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(0);
}
};
const createUiGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(i);
}
let counter = -1;
const result = cells.map((cell) => {
counter++;
return <div className="cell" id={counter} />;
});
setHumanSetupGrid(result);
};
const setUpPlayerGrid = () => {
// createGrid('grid');
createUiGrid();
};
const currentShip = () => {
return ships[placedShips];
};
const clickListener = (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(e.target.id);
let end = start + currentShip().length - 1;
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
playerGameboard.placeShip(placedShips, direction, start, end);
setPlacedShips((oldValue) => oldValue + 1);
console.log(placedShips);
}
};
const setEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.addEventListener("click", (e) => {
clickListener(e);
});
cell.addEventListener("mouseover", (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(cell.id);
let end = start + currentShip().length - 1;
if (currentShip().direction === "horizontal") {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i++) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.add("test");
});
}
} else {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i += 10) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.add("test");
});
}
}
});
cell.addEventListener("mouseleave", (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(cell.id);
let end = start + currentShip().length - 1;
if (currentShip().direction === "horizontal") {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i++) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.remove("test");
});
}
} else {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i += 10) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.remove("test");
});
}
}
});
});
};
const removeEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.removeEventListener("click", (e) => {
clickListener(e);
});
});
};
useEffect(() => {
setUpPlayerGrid();
// setUpComputerGrid();
}, []);
useEffect(() => {
console.log(humanSetupGrid);
}, [humanSetupGrid]);
// Re-render the component to enable event listeners to be added to generated grid
useLayoutEffect(() => {
setEventListeners();
});
useEffect(() => {
removeEventListeners();
setEventListeners();
console.log(placedShips);
}, [placedShips]);
return (
<div className="setup-container">
<div className="setup-information">
<p className="setup-information__p">Add your ships!</p>
<button
className="setup-information__btn"
onClick={() => console.log(placedShips)}
>
Rotate
</button>
</div>
<div className="setup-grid">
<Table grid={humanSetupGrid} />
</div>
</div>
);
};
export default GameboardSetup;
I am quite confused what is happening here and have been stuck on this problem for a couple of days now - if anybody has any suggestions then they would be highly appreciated!
Thank you.
const removeEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.removeEventListener("click", (e) => {
clickListener(e);
});
});
};
The above code does not remove any event listeners, which is probably the reason why 0 is still being logged. You pass a new anonymous function to removeEventListener. Since the function is just created it will never remove any event listeners, because it is not registered as an event listener.
Two different functions that do the same are not equal, which is why the event listener is not removed.
const a = (e) => clickListener(e); // passed to addEventListener
const b = (e) => clickListener(e); // passed to removeEventListener
console.log(a == b); //=> false
To add and remove events you cannot use anonymous functions. You either have to use named functions, or store the function in a variable. Then register and remove the event listener using the function name or variable.
Since you only forward the event to the clickListener you can simply replace your event handler registration with:
cell.addEventListener("click", clickListener);
Then remove it using:
cell.removeEventListener("click", clickListener);
Note that this scenario could've been avoided if you passed your event handlers using a more React approach. Instead of using cell.addEventHandler(...) you could've passed the event on creation of this element. eg. <div className='cell' id={counter} onClick={clickListener} />
When working with React you should try to not manipulate the DOM manually. React Components have Synthetic Events, which means that you don't need to add event listeners the vanilla way.
Just add each synthetic event to the cell component with its corresponding handler.
You can do it in the createUiGrid function:
const createUiGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(i);
}
let counter = -1;
const result = cells.map((cell) => {
counter++;
return <div className="cell" id={counter} onClick={onClickHandler} onMouseOut={onMouseOutHandler} onMouseOver={onMouseOverHandler} />;
});
setHumanSetupGrid(result);
};
And then just move the code you did on vanilla to each corresponding handler (be sure to remove all listener manipulation before testing).

Optimize array of component which have window event listeners

I have array of data, which needed to render array of one and the same component which has window event listener. But if we have 20 event listeners on page then we have super freezing on page. How to have one event listener on array of one and the same component?
class BmResponsiveMenuButton extends PureComponent {
lastWidth = 0
resizeStep = 0
state = {
hidden: false
}
componentDidMount() {
//HERE ADDING EVENT LISTENERS (TOTALLY WE HAVE 15-20 LISTENERS ON PAGE)
window.addEventListener('resize', this.handleResize)
window.addEventListener('scroll', this.handleScroll, false)
this.anchorMenuButton = document.getElementById(this.props.moreMenuButtonId)
}
handleResize = () => {
this.handleHiddenOrNot()
}
componentDidUpdate() {
this.lastWidth = document.documentElement.clientWidth
}
handleHiddenOrNot = () => {
this.resizeStep = Math.abs(this.lastWidth - document.documentElement.clientWidth)
let boundingMenu = this.anchorMenuButton.getBoundingClientRect()
let boundingButton = this.anchorEl.getBoundingClientRect()
if (boundingButton.right > boundingMenu.left - 10) {
this.setState({
hidden: true
})
this.props.onHide(this.props.id)
} else {
this.setState({
hidden: false
})
this.props.onUnHide(this.props.id)
}
}
handleScroll = () => {
this.handleResize()
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize)
window.removeEventListener('scroll', this.handleScroll, false)
}
handleExited = () => {
let elem = this.anchorEl
elem.style.opacity = '0'
elem.style.visibility = 'hidden'
}
handleEnter = () => {
let elem = this.anchorEl
elem.style.opacity = '1'
elem.style.visibility = 'visible'
elem.style.position = 'sticky'
}
render() {
const {
title,
url,
classes,
target,
effects,
effectType,
index,
active,
currentModule,
module
} = this.props
const transitionProps = {
key: title,
in: !this.state.hidden,
enter: true,
exit: true,
timeout: !effects ?
0 : {
enter: this.resizeStep > 200 ? index * 100 : 300,
exit: 200
},
onEnter: () => this.handleEnter(),
onExited: () => this.handleExited()
}
let activeModule
if (module && currentModule === module) {
activeModule = true
} else if (active) {
activeModule = active
} else {
activeModule = url.includes(window.location.pathname + window.location.search + window.location.hash)
}
const ButtonComponent = ( <
Link to = {
url
}
target = {
target
}
innerRef = {
(node) => (this.anchorEl = node)
} >
<
Button className = {
classNames(classes.topNavigationButton, activeModule ? classes.selected : '')
} > {
title
} <
/Button> < /
Link >
)
switch (effectType) {
case 'slide':
return ( <
Slide direction = {
'left'
} { ...transitionProps
} > {
ButtonComponent
} <
/Slide>
)
case 'fade':
return <BmFade { ...transitionProps
} > {
ButtonComponent
} < /BmFade>
case 'grow':
return <BmGrow { ...transitionProps
} > {
ButtonComponent
} < /BmGrow>
default:
break
}
}
}
Edited with code example. I use this component for displaying or hiding button if it not fit in window. I comment place where i create eventListeners

Error while calculating index value from props React (works when hardcoded)

I am using this to create a 3D interactive view for a product: https://github.com/aldrinc/React360. The code in question is:
import React, { Component } from "react";
import "./React360.css";
// You can play with this to adjust the sensitivity
// higher values make mouse less sensitive
const pixelsPerDegree = 3;
class React360 extends Component {
static defaultProps = { dir: 'awair-360', numImages: 55 };
state = {
dragging: false,
imageIndex: 0,
dragStartIndex: 0
};
componentDidMount = () => {
document.addEventListener("mousemove", this.handleMouseMove, false);
document.addEventListener("mouseup", this.handleMouseUp, false);
};
componentWillUnmount = () => {
document.removeEventListener("mousemove", this.handleMouseMove, false);
document.removeEventListener("mouseup", this.handleMouseUp, false);
};
handleMouseDown = e => {
e.persist();
this.setState(state => ({
dragging: true,
dragStart: e.screenX,
dragStartIndex: state.imageIndex
}));
};
handleMouseUp = () => {
this.setState({ dragging: false });
};
updateImageIndex = currentPosition => {
let numImages = this.props.numImages;
const pixelsPerImage = pixelsPerDegree * (360 / numImages);
const { dragStart, imageIndex, dragStartIndex } = this.state;
// pixels moved
let dx = (currentPosition - dragStart) / pixelsPerImage;
let index = Math.floor(dx) % numImages;
if (index < 0) {
index = numImages + index - 1;
}
index = (index + dragStartIndex) % numImages;
// console.log(index, dragStartIndex, numImages)
if (index !== imageIndex) {
this.setState({ imageIndex: index });
}
};
handleMouseMove = e => {
if (this.state.dragging) {
this.updateImageIndex(e.screenX);
}
};
preventDragHandler = e => {
e.preventDefault();
};
renderImage = () => {
const { imageIndex } = this.state;
if (isNaN(imageIndex)) {
this.setState({imageIndex: 0})
return
}
return (
<div className="react360">
<img
className="react-360-img"
alt=""
src={require(`./${this.props.dir}/${imageIndex}.jpg`)}
/>
</div>
);
};
render = () => {
return (
<div
className="react-360-img"
onMouseDown={this.handleMouseDown}
onDragStart={this.preventDragHandler}
>
{this.renderImage()}
</div>
);
};
}
export default React360;
I am running into an issue where hardcoding the variable for the number of images (numImages) results in proper function but when I set the number of images as a prop let numImages = this.props.numImages; my image index sometimes goes to NaN. I've made a hacky workaround by setting the imageIndex state to 0 if it is NaN but I would like to resolve this issue correctly by understanding what exactly is different with setting a variable using props vs hardcoding.

When trying access event.target.$something always return 'undefined'

Im trying to set an event handler for scroll,
I found in many sources the simple line of:
const bottom = e.target.scrollHeight - e.target.scrollTop ===
e.target.clientHeight;
but for some reason , its always return undefined:
handleScroll = (e) => {
console.log('inside hanglescroll');
const bottom = e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight;
console.log(e.target.scrollHeight); **print undefined!!**
console.log(e.target.scrollTop); **print undefined!!**
console.log(e.target.clientHeight); **print undefined!!**
if (bottom) {
console.log('we are in the bottom');
}
}
componentDidMount() {
window.addEventListener('scroll',this.handleScroll.bind(this));
}
Thank you!!
This happens because in this case e.target === document, which is Document, not DOM element. While DOM element is reachable as document.documentElement.
This should work:
handleScroll = (e) => {
const el = e.target.documentElement;
const bottom = el.scrollHeight - el.scrollTop === el.clientHeight;
if (bottom) {
console.log('we are in the bottom');
}
}
componentDidMount() {
window.addEventListener('scroll',this.handleScroll);
}
Notice that .bind(this) doesn't do anything with arrow functions and isn't needed. It's either an arrow or prototype method with bind.
Ideally,it should be document instead of e.Don't need to bind the arrow function,because it implicitly the binds the callee context.
handleScroll = (e) => {
let scrollTop = (document.documentElement && document.documentElement.scrollTop) ||
document.body.scrollTop;
let scrollHeight = (document.documentElement &&
document.documentElement.scrollHeight) || document.body.scrollHeight;
let clientHeight = document.documentElement.clientHeight || window.innerHeight;
let scrolledToBottom = Math.ceil(scrollTop + clientHeight) >= scrollHeight;
console.log(scrolledToBottom);
}
componentDidMount = () => {
window.addEventListener('scroll', this.handleScroll, true);
};
handleScroll = () => {
const end =
document.documentElement.scrollHeight -
document.documentElement.scrollTop ===
document.documentElement.clientHeight;
if (end) {
console.log('e,', end);
}
};

Resources