Cannot read property 'style' of undefined in useEffect - javascript

Currently I'm trying to change the layout of my webpage according to the width of the webpage. So if it is higher that a certain pixels, it should show a different view, but if its lower, then it should show a different view. To achieve this I have tried using useState and useEffect to get the window.innerWidth and then placing conditions on my return statement but this brings up a Cannot read property 'style' of undefined.
Code:
export default function App() {
const [screen, setScreen] = useState(false);
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
});
const ref = useRef(null);
// Reduce value if want the image to be closer to the edges
// otherwise to the center
const setImageLimitMovement = 1;
const setTextLimitMovement = 4;
const opacityRange = 400;
// Speed text movement
const speed = 1; // .5
useEffect(() => {
window.addEventListener("resize", () => {
if (window.innerWidth !== 0) {
setScreen(window.innerWidth);
}
});
}, []);
useEffect(() => {
const app = [...ref.current.children];
const titles = app.filter((el) => el.matches(".titles") && el);
const blocks = app.filter((el) => el.matches(".blocks") && el);
const img = app.find((el) => el.matches("#passport") && el);
// Get the center point of blocks in an array
const centerPoints = blocks.map((blockEl, idx) => {
const blockindex = idx + 1;
const blockHeight = Math.floor(blockEl.getBoundingClientRect().height);
const blockHalf = blockHeight / 2;
return blockHeight * blockindex - blockHalf;
});
const leftMoveLimitImg = -centerPoints[0] / setImageLimitMovement;
const rightMoveLimitImg = centerPoints[0] / setImageLimitMovement;
const textLimit = centerPoints[0] / setTextLimitMovement;
const changeBackground = () => {
const value = window.scrollY;
titles[0].style.transform = `translateY(-${value * speed}px)`;
// IMAGE BOUNCE
// Move to <==
if (centerPoints[0] > value) {
img.style.transform = `translateX(-${
value * (1 / setImageLimitMovement)
}px)`;
titles[1].style.transform = `translateX( ${
0 + value / setTextLimitMovement
}px)`;
titles[1].style.opacity = value / opacityRange;
return;
}
window.requestAnimationFrame(changeBackground);
};
window.addEventListener("scroll", changeBackground);
return () => window.removeEventListener("scroll", changeBackground);
}, [screen]);
return (
<>
{/* <div style={{height:"100vh"}}></div> */}
{width > 650 && (
<div id="section2">
<main ref={ref}>
<h1 id="title" className="titles">
{posts.Title}
</h1>
<section id="block1" className="blocks"></section>
<figure id="passport">
<img
alt="passport"
src="https://cdn.britannica.com/87/122087-050-1C269E8D/Cover-passport.jpg"
/>
</figure>
<h2 id="text1" className="titles text1">
Random Text 1
</h2>
</main>
</div>
)}
{width < 649 && (
<>
<div style={{ height: "100vh", backgroundColor: "black" }}></div>
</>
)}
{/* Stop Scrolling Animation */}
{/* <div>Content</div> */}
</>
);
}

The problem as identified by #lawrence-witt is because the ref object is not yet set when the useEffect runs the first time.
Here is the codesandbox link https://codesandbox.io/s/infallible-lamarr-usim6
I added some comments as I did a bit of refactor, but please feel free to pick what solves your problem.

Related

React iterating over list and applying animation with delay between each iteration

I am having troubles fixing a bug basically I need to iterate over a list of buttons and apply an animation and on the next iteration I remove the animation from the previous element, however, when running the code the animation is started twice at the beginning and one element remains stuck with the animation applied.
The following is the code of the component:
import type { NextPage } from 'next'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
import { IoCheckmark, IoClose, IoHome, IoRefresh } from 'react-icons/io5'
import Page from '../components/page/Page'
import styles from '../styles/Play.module.css'
import { distance } from '../utils/distance'
import { randomInt, randomFloat } from '../utils/random'
function ShowSequence(props: any) {
const [index, setIndex] = useState(0);
const [sequence, setSequence] = useState(props.sequence);
const [timer, setTimer] = useState<any>();
useEffect(() => {
console.log(index)
if (index > 0) document.getElementById(sequence[index - 1])?.classList.toggle(styles.animate);
if (index < sequence.length) document.getElementById(sequence[index])?.classList.toggle(styles.animate);
else return clearInterval(timer);
setTimer(setTimeout(() => setIndex(index + 1), 3000));
}, [index]);
return <div className={styles.button}>
{
props.map ? props.map.map((button: any) => {
return <button key={button.buttonId} className={styles.button} id={button.buttonId} style={{ top: button.y + "px", left: button.x + "px", backgroundColor: button.color }}></button>
}) : null
}
</div>;
}
function DoTask(props: any) {
return <div>
</div>;
}
function ChooseSequence(props: any) {
const [sequence, setSequence] = useState(props.sequence);
const [index, setIndex] = useState(0);
const [timer, setTimer] = useState<any>();
const [buttonMap, setButtonMap] = useState<any>({});
console.log(sequence);
return <div className={styles.button}>
{
props.map ? props.map.map((button: any) => {
return <button key={button.buttonId} className={styles.button} id={button.buttonId} style={{ top: button.y + "px", left: button.x + "px", backgroundColor: button.color }} onClick={(e) => {
let correctSequence = sequence[index] === button.buttonId;
e.currentTarget.classList.toggle(correctSequence ? styles.correctButton : styles.wrongButton);
buttonMap[button.buttonId] = correctSequence ? <IoCheckmark size={20} color={"white"}></IoCheckmark> : <IoClose size={20} color={"white"}></IoClose>;
setButtonMap(buttonMap);
setIndex(index + 1);
}}>
{ (buttonMap[button.buttonId]) ? buttonMap[button.buttonId] : button.buttonId }
</button>
}) : null
}
</div>;
}
function Error(props: any) {
return <div className={styles.errorMenu}>
<h1>You lost!</h1>
<p>You reached level: {props.level}</p>
<div className={styles.container}>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
</div>
<div className={styles.row}>
<button className={styles.retryButton} onClick={() => window.location.href = "/play"}><IoRefresh></IoRefresh></button>
<button className={styles.closeButton} onClick={() => window.location.href = "/"}><IoHome></IoHome></button>
</div>
</div>;
}
enum State {
SHOWSEQUENCE,
DOTASK,
CHOOSESEQUENCE,
ERROR
}
const Play: NextPage = () => {
let [state, setState] = useState<State>(State.SHOWSEQUENCE);
let [sequence, setSequence] = useState<number[]>([randomInt(1, 20), randomInt(1, 20), randomInt(1, 20), randomInt(1, 20)]);
let [map, setMap] = useState<any[]>();
let [level, setLevel] = useState(1);
let component;
useEffect(() => {
if (state === State.SHOWSEQUENCE) {
let newSequenceId = randomInt(1, 20);
setSequence((prevSequence: number[]) => [...prevSequence, newSequenceId])
}
}, [state]);
useEffect(() => {
let buttonIds = Array.from({ length: 20 }, (v, k) => k + 1);
const { innerWidth, innerHeight } = window;
let colors: string[] = ["#c0392b", "#e67e22", "#27ae60", "#8e44ad", "#2c3e50"];
let buttonMap: any[] = [];
let rows = buttonIds.length / 10;
let columns = rows > 0 ? buttonIds.length / rows : buttonIds.length;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < columns; col++) {
let color = colors[Math.floor(randomFloat() * colors.length)];
let x = innerWidth / columns * col + 100;
let y = innerHeight / rows * row + 100;
let offsetX = (randomFloat() < .5) ? -1 : 1 * randomFloat() * ((innerWidth / columns) - 100);
let offsetY = (randomFloat() < .5) ? -1 : 1 * randomFloat() * ((innerHeight / rows) - 100);
if (x + offsetX + 100 > innerWidth) offsetX -= ((x + offsetX) - innerWidth) + 100;
if (y + offsetY + 100 > innerHeight) offsetY -= ((y + offsetY) - innerHeight) + 100;
buttonMap.push({ buttonId: buttonIds[row * columns + col], x: x + offsetX, y: y + offsetY, color })
}
}
setMap(buttonMap);
}, [])
switch (state) {
case State.SHOWSEQUENCE:
component = <ShowSequence map={map} sequence={sequence} changeState={() => setState(State.DOTASK)}></ShowSequence>;
break;
case State.DOTASK:
component = <DoTask changeState={() => setState(State.CHOOSESEQUENCE)} onError={() => setState(State.ERROR)}></DoTask>
break;
case State.CHOOSESEQUENCE:
component = <ChooseSequence map={map} sequence={sequence} changeState={() => setState(State.SHOWSEQUENCE)} onError={() => setState(State.ERROR)}></ChooseSequence>
break;
}
return (
<Page color="blue">
{ state === State.ERROR ? <Error level={level}></Error> : null }
{component}
</Page>
)
}
export default Play
Here is a codesandbox.
Instead of querying the DOM, an anti-pattern in React, you should add the appropriate classname when mapping the data. This avoids the DOM mutations and handles the conditional logic for adding and removing the "animate" class.
Use a functional state update to increment the index.
I suggest using a React ref to hold a reference to the interval timer so it's not triggering additional rerenders
Return a cleanup function to clear any running timers when necessary, i.e. when the component unmounts.
Code:
function ShowSequence(props) {
const [index, setIndex] = useState(0);
const timerRef = useRef();
useEffect(() => {
console.log(index);
timerRef.current = setTimeout(() => setIndex((index) => index + 1), 3000);
return () => {
clearTimeout(timerRef.current);
};
}, [index]);
return (
<div className={styles.button}>
{props.map?.map((button, i) => {
return (
<button
key={button.buttonId}
className={[styles.button, i === index ? styles.animate : null]
.filter(Boolean)
.join(" ")}
id={button.buttonId}
style={{
top: button.y + "px",
left: button.x + "px",
backgroundColor: button.color
}}
></button>
);
})}
</div>
);
}
To cleanup the timer in the useEffect, you must return a function.
The previous element index is 1 less the current index for non-zero indexes or the last element in the array of buttons when the current index is the first item.
const prevElIndex = index == 0 ? props.map.length - 1 : index - 1;
Also, you need to check if the previous element has the animation class before toggling the class. This takes care of the first time the animation starts to run (the previous element would not have the animation class).
if (
document
.getElementById(sequence[prevElIndex])
?.classList.contains(styles.animate)
) {
document
.getElementById(sequence[prevElIndex])
?.classList.toggle(styles.animate);
}
Altogether, your effect would be along these lines:
const [index, setIndex] = useState(0);
const [sequence, setSequence] = useState(props.sequence);
const [timer, setTimer] = useState<number>();
useEffect(() => {
const prevElIndex = index == 0 ? props.map.length - 1 : index - 1;
if (
document
.getElementById(sequence[prevElIndex])
?.classList.contains(styles.animate)
) {
document
.getElementById(sequence[prevElIndex])
?.classList.toggle(styles.animate);
}
document.getElementById(sequence[index])?.classList.toggle(styles.animate);
setTimer(setTimeout(() => setIndex((index + 1) % sequence.length), 3000));
return () => clearTimeout(timer);
}, [index]);
A working Stackblitz showing this in action.

Anchor link not working when manipulating scroll via JS

I'm creating a landing page and it has a bar to navigate through the website sections. But, it's not working and I gathered some resources that made me believe it's because of the Infinite Slide animation I did on the website.
The Anchor tags only navigate when window.clientWidth >= 1920.
I have two Infinite section sliders that load the animation, adding +.5 to scrollLeft.
So when the view goes below the enable boundary (<1920px), the anchor links stop working. Is it possible to make them work again without removing the animations?
export const SliderContainer: React.FC<SliderProps> = ({
children,
initialOffsetX,
className,
contentWidth,
contentClassName,
}) => {
const { innerWidth } = useSize();
const refScrollX = useRef<number>(initialOffsetX);
const refContainer = useRef<HTMLDivElement>(null);
const refContent = useRef<HTMLDivElement>(null);
const enabled = innerWidth < contentWidth;
useAnimationFrame(
enabled,
useCallback(() => {
const { current: elContainer } = refContainer;
const { current: elContent } = refContent;
if (elContainer && elContent) {
refScrollX.current += 0.5;
elContainer.scrollLeft = refScrollX.current;
if (elContainer.scrollLeft >= elContent.clientWidth) {
refScrollX.current = 0;
elContainer.scrollLeft = 0;
}
}
}, [])
);
const cn = "inline-block".concat(" ", contentClassName || "");
return (
<div ref={refContainer} className={`overflow-x-hidden whitespace-nowrap max-w-full pointer-events-none ${className}`}>
<div ref={refContent} className={cn}>
{children}
</div>
<div className={enabled ? cn : "hidden"}>{children}</div>
</div>
);
};

How to set new value in two dimensional array with useState()

See here for a Codesandbox displaying my problem. Try typing a few words in both the textareas to see what is going wrong.
I'm new to React. I am trying to dynamically create a svg element based on user input. Im splitting up the individual words, calculating the width of these words, and push them into a content array.
Without using the useState React hook (left side on Codesandbox), this all works fine. I just declared a variable const content = [[]] and push the individual words to the array. However, for some reason when using useState (const [content, setContent] = useState([[]])), the array is updated totally different.
How can I make my (left) example from the Codesandbox example work with the React useState hook (right example from Codesandbox) and what am I doing wrong?
So I fixed your code the way you have it working without using useState hook for the content. I created two new states and used useEffect as well which felt necessary.
When state changes component re-renders, so you need to be careful about the state changes, in your case you don't need to copy the state of content into the newContent instead you need to re-initialize it same as you did at the top when initializing the state.
Similarly you also need to use useEffect hook where you can run a part of code which is dependent on some variable in your case it's dummyText, wordsWithComputedWidth, spaceWidth on separate locations.
import { useState, useEffect } from "react";
export default function WithUseState() {
const [dummyText, setDummyText] = useState("");
const [wordsWithComputedWidth, setWordsWithComputedWidth] = useState(1);
const [spaceWidth, setSpaceWidth] = useState(0);
const [content, setContent] = useState([[]]);
function calculateWordWidths(v) {
const words = v.split(/\s+/);
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
let text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.style =
"font: 10pt LiberationSans, Helvetica, Arial, sans-serif; white-space: pre;";
svg.appendChild(text);
document.body.appendChild(svg);
const wordsWithComputedWidth = words.map((word) => {
text.textContent = word;
return { word, width: text.getComputedTextLength() };
});
text.textContent = "\u00A0"; // Unicode space
const spaceWidth = text.getComputedTextLength();
document.body.removeChild(svg);
return { wordsWithComputedWidth, spaceWidth };
}
useEffect(() => {
let { wordsWithComputedWidth, spaceWidth } = calculateWordWidths(dummyText);
setWordsWithComputedWidth(wordsWithComputedWidth);
setSpaceWidth(spaceWidth);
}, [dummyText]);
useEffect(() => {
let xPos = 0;
let yPos = 16;
let page = 1;
let i = 0;
let len = wordsWithComputedWidth.length;
let newContent = [[]];
while (i < len) {
if (yPos > 25) {
newContent.push([]);
page++;
xPos = 0;
yPos = 16;
}
newContent[page - 1].push(
<text
x={xPos}
y={yPos}
key={i + xPos + yPos + wordsWithComputedWidth[i].word}
style={{
font: "10pt LiberationSans, Helvetica, Arial, sans-serif",
whiteSpace: "pre"
}}
>
{wordsWithComputedWidth[i].word}
</text>
);
xPos += wordsWithComputedWidth[i].width + spaceWidth;
if (xPos > 25) {
yPos += 16;
xPos = 0;
}
i++;
}
setContent(newContent);
}, [wordsWithComputedWidth, spaceWidth]);
function renderContentStructure() {
return content.map((page, pageIdx) => {
return (
<>
<span style={{ fontWeight: "bold" }}>Page: {pageIdx}</span>
<ol key={pageIdx}>
{page.map((fields, fieldIdx) => {
return <li key={fieldIdx}> {fields} </li>;
})}
</ol>
</>
);
});
}
return (
<div className="with-use-state">
<div>
Currently NOT working <strong>With</strong> useState
</div>
<div style={{ marginTop: "50px" }}>
<div className="content">
<div>Content</div>
<textarea
rows="6"
cols="24"
onChange={(e) => setDummyText(e.target.value)}
placeholder="type here..."
/>
</div>
<div className="preview">
<div>Preview</div>
<>
{content.map((page, idx) => (
<svg
viewBox="0 0 50 50"
xmlns="http://www.w3.org/2000/svg"
style={{
border: "1px solid #aaa",
width: "100px",
display: "block"
}}
key={idx}
>
{page}
</svg>
))}
</>
</div>
<div>{renderContentStructure()}</div>
</div>
</div>
);
}

React Animations - change only one logo at a time from a list of 6 initial logos displayed?

I am working on a feature where I need to change only one logo at a time out of a list of 6 logos displayed initally.
The example is shown here:: https://www.loom.com/share/6a282423368a46418248a789ce4fc139
And its there in this website also in the bottom:: https://www.wonderlandams.com/about?fbclid=IwAR0wfFrqYVwor1UJfZGcWK2MaU0kBWNiaacg8kGb_IC--VaziorY6BDt7lA.
import React, { useState, useEffect } from 'react'
import tw from 'twin.macro'
import Image from './image'
const LogoGrid = ({ logos, style }) => {
const groupDisplay = logos.slice(0, 6);
const [group, setGroup] = useState(groupDisplay);
const [groupLength, setGroupLength] = useState(0);
let shuffledLogos, i;
const shuffle = (array) => array.sort(() => Math.random() - 0.5);
useEffect(() => {
console.log("UEFFECT RUNNING");
const timer = setInterval(() => {
i = Math.floor(Math.random() * 6);
shuffledLogos = shuffle(logos);
let gl = groupLength + 1 ;
if(groupDisplay[i] == logos[i]) {
let k = i + 1;
groupDisplay[i] = shuffledLogos[k];
}
else {
groupDisplay[i] = shuffledLogos[i];
}
setGroupLength(gl)
setGroup(groupDisplay)
}, 2000)
return () => clearInterval(timer)
}, [group, groupLength])
return (
<div css={[tw`relative`, style]}>
<div
css={[
tw`opacity-0 grid-cols-3 grid-rows-2 gap-12
lg:(gap-x-16 gap-y-12 mt-26) xl:gap-x-32`,
tw`grid transition transition-opacity duration-300 ease-in-out opacity-100`,
]}
>
{(group || []).map((logo, index) => (
<div key={index} css={tw`h-12`}>
<Image image={logo} />
</div>
))}
</div>
</div>
)
}
export default LogoGrid
I have made one component using tailwind css but it does not work as expected. Can anyone kindly point me in the right direction or any changes in the logic that can give me similar effects ?

Logic for rotating a square

The following code is using React.
I have a square positioned at 45deg, that has four elements. Upon click on each element, I want the square to rotate the relevant degrees so that the clicked element is on top. (In the image, 3 is currently selected).
Please see the notes within the code. I hope it explains what my logic is.
Here is the component code:
class CentreCtrls extends React.Component {
constructor(props) {
super(props);
this.state = {
aboutUs: {
activeCtrl: 0
}
}
}
// animate the square to rotate the relevant degrees
// so that the clicked element is at the top.
// this logic is flawed and not working correctly
componentDidUpdate() {
const activeHeading = this.state.aboutUs.activeCtrl;
const { centreCtrl } = this.refs;
const style = centreCtrl.style;
const currentDeg = parseInt(style.transform.slice(7, -4), 10);
const position = this.getPosition(activeHeading, 0);
const degRotation = position * 90;
console.log('pos:', position, 'deg:', currentDeg, '-', degRotation);
anime({
targets: this.refs.centreCtrl,
rotate: `-${currentDeg + degRotation}deg`,
duration: 150,
easing: 'linear',
});
}
onClickHandler = e =>
const ele = e.target;
const i = ele.parentNode.getAttribute('data-i');
this.setState({
aboutUs: {
activeCtrl: parseInt(i, 10),
},
});
};
// the purpose of this function is to find the current value
// to be assigned to the 'data-i' property on element, this 'data-i'
// is read in the above 'componentDidUpdate' function to animate
// the square the relevant degrees so that the clicked square is at
// top (where the purple 3 is in the image).
getPosition = (i, activeHeading) => {
const total = 3; // start: 0, ttl: 4
let correctSeq = i;
if (i === activeHeading) {
correctSeq = 0;
// if i == 2, then for our rotation purpose, its at position 3 in DOM
// because of the way, DOM renders elements from left to right within
// square
} else if (i === 2) {
correctSeq = 3;
} else if (i === 3) {
correctSeq = 2;
}
let pos = total - activeHeading + (correctSeq + 0);
if (pos > 3) {
pos = pos - 3;
}
return pos;
};
render() {
const { data } = this.props;
const activeHeading = this.state.aboutUs.activeCtrl;
return (
// using the react v15 style ref
<div
className="centreCtrl"
ref="centreCtrl"
style={{ transform: 'rotate(45deg)' }}
>
{data.map((entry, i) => {
const j = this.getPosition(i, activeHeading);
return (
<div
className="ctrl"
key={i}
data-i={j}
id={`${entry.narrative.heading}`}
onClick={this.onClickHandler}
>
<div
className="textContainer"
id={entry.narrative.heading}
ref={`textRef${i}`}
>
{j}
</div>
</div>
);
})}
</div>
);
}
}
Edit: How can I get the rotation logic to work correctly. More to the point, I am looking for input into what is the best logic for rotation calculation of this square. It looks far easier than I estimated.
Assuming you want to rotate clockwise, something like this might help:
import React from "react";
import ReactDOM from "react-dom";
import styled from "styled-components";
import { Container, Square, Block, Top, Bottom } from './components';
const rotateRight = items => {
const last = items.pop();
items.unshift(last);
};
class App extends React.Component {
state = {
items: ["a", "b", "c", "d"],
rotation: 0,
};
handleClick = i => {
let start = i;
let count = 0;
let items = [...this.state.items];
const end = items.length - 1;
while (start <= end) {
rotateRight(items);
start += 1;
count += 1;
}
this.setState(state => ({
rotation: ((count * 90) % 360),
}));
};
render() {
const { items, rotation } = this.state;
return (
<Container>
<Square rotation={rotation}>
<Top>
<Block onClick={() => this.handleClick(0)}>
{items[0]}
</Block>
<Block onClick={() => this.handleClick(1)}>
{items[1]}
</Block>
</Top>
<Bottom>
<Block onClick={() => this.handleClick(2)}>{
items[2]}
</Block>
<Block onClick={() => this.handleClick(3)}>
{items[3]}
</Block>
</Bottom>
</Square>
</Container>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
StackBlitz link here.

Categories