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

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>
);
}

Related

Cannot read property 'style' of undefined in useEffect

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.

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 ?

More optimized way to render multiple videos simultaneously on React Native / Expo?

I am working on a video-based app. I am only showing one video at a time but am rendering multiple videos simultaneously.
The reason why I'm rendering multiple videos at a time is so that users can go to one video to a second video without waiting for the second video to load.
I am new to React Native and starting to face performance issues with rendering too many videos. Is there a more optimal way of rendering than the way I'm doing it below?
I can't really tell whether I'm changing data that should be immutable or not avoiding unnecessary re-renders.
I start off by setting a constant renderedVideos.
const renderedVideos = [];
I have some logic to process some queried data. I ultimately push each video component into the array renderedVideos.
renderedVideos.push(
<Video
ref={playbackObject}
source={{uri: source}}
rate={1.0}
volume={1.0}
isMuted={isMuted}
resizeMode="cover"
usePoster={true}
shouldPlay={shouldPlay}
onPlaybackStatusUpdate={_onPlaybackStatusUpdate}
progressUpdateIntervalMillis={50}
isLooping
style={videoStyle}
>
</Video>
)
I then call the renderedVideos within a View component.
<View>
{renderedVideos}
</View>
Is there a more optimal way to render these videos? Thanks so much! If you'd like to see the whole file, please see below :)
import React, { useEffect, useRef, useState } from 'react';
import { Video, Audio } from 'expo-av';
import { View, Animated } from 'react-native';
export default function MultipleVideos(props) {
const renderedGroup = props.limit;
let initialIndex;
let indexLimit;
if (props.first) {
const renderedIndex = Math.round(props.userIndex / renderedGroup);
initialIndex = renderedIndex * renderedGroup;
indexLimit = renderedIndex * renderedGroup + renderedGroup / 2;
} else {
const renderedIndex = Math.floor(props.userIndex / renderedGroup);
initialIndex = renderedIndex * renderedGroup + renderedGroup / 2;
indexLimit = renderedIndex * renderedGroup + renderedGroup;
}
const playingData = {
isMuted: false,
shouldPlay: props.shouldPlay,
playbackObject: props.playbackObject,
_onPlaybackStatusUpdate: props._onPlaybackStatusUpdate,
display: "flex"
}
const nonplayingData = {
isMuted: true,
shouldPlay: false,
playbackObject: null,
_onPlaybackStatusUpdate: null,
display: "none"
}
useEffect(() => {
Audio.setAudioModeAsync({
playsInSilentModeIOS: true
});
}, []);
const renderedVideos = [];
if(props.videoData) {
for(let i = initialIndex; i < indexLimit; i++){
const user = props.videoData.users[i];
if(user){
for (let j = 0; j < user.userVideos.length; j++){
const muxPlaybackId = user.userVideos[j].muxPlaybackId;
const muxPlaybackUrl = 'https://stream.mux.com/' + muxPlaybackId + '.m3u8';
if(props.userIndex == i && props.videoIndex == j){
renderedVideos.push(
<PlayingVideo
key={muxPlaybackId}
playbackObject={playingData.playbackObject}
source={muxPlaybackUrl}
isMuted={playingData.isMuted}
shouldPlay={playingData.shouldPlay}
_onPlaybackStatusUpdate={playingData._onPlaybackStatusUpdate}
display={playingData.display}
/>
)
}
else {
renderedVideos.push(
<PlayingVideo
key={muxPlaybackId}
playbackObject={nonplayingData.playbackObject}
source={muxPlaybackUrl}
isMuted={nonplayingData.isMuted}
shouldPlay={nonplayingData.shouldPlay}
_onPlaybackStatusUpdate={nonplayingData._onPlaybackStatusUpdate}
display={nonplayingData.display}
/>
)
}
}
}
}
}
return(
<View>
{renderedVideos}
</View>
)
}
function PlayingVideo({playbackObject, source, isMuted, shouldPlay, _onPlaybackStatusUpdate, display}){
const videoStyle = { width: '100%', height: '100%', display: display};
return(
<Video
ref={playbackObject}
source={{uri: source}}
// posterSource={{ uri: posterSource}}
// posterStyle={{ width: '100%', height: '100%', display: display, flex: 1 }}
rate={1.0}
volume={1.0}
isMuted={isMuted}
resizeMode="cover"
usePoster={true}
shouldPlay={shouldPlay}
onPlaybackStatusUpdate={_onPlaybackStatusUpdate}
progressUpdateIntervalMillis={50}
isLooping
style={videoStyle}
>
</Video>
)
}

How can I re-position a canvas element dynamically with CSS

I've written a piece of code in React and with mouse-click, I position a canvas element, I change 3 style property of the canvas element with mouse-click. When the page refreshes I am getting this canvas elements position from the database and try to change it again according to data coming from the database however it doesnt change any property but if i try to change 1 property it changes, what might be causing this?
Here is the relevant part of my code:
function click(e) {
if(upgrade_status){
if ( material[x_cord + "" + y_cord] == null) {
if(neededmat>totalmats){
//
}else{
setStyleProp('left', x_cord*90+30);
setStyleProp('top', y_cord*120+150);
setStyleProp('backgroundColor', "white");
}
}else{
if(mat ===source){
setStyleProp('left', x_cord*90+30);
setStyleProp('top', y_cord*120+150);
setStyleProp('backgroundColor', "white");
}
if(neededmat>totalmats){
//
}else{
//
}
}else{
//errormessage1
}
}
}else{
//errormessage2
}
}
function Economy() {
ref = useRef();
ref2 = useRef();
ref_yazi = useRef();
const kullanici =useTimes()
console.log("kaƧkere")
setStyleProp = (prop, value) => {
setStyle({
...style,
[prop]: value
})
}
const [style, setStyle] = useState({
position: "absolute",
border: "0px solid red",
zIndex: "3",
width: "35px",
height: "25px",
backgroundColor:"transparent"
});
useInterval(() => {
setCount(count + 1);
}, 1000);
let [count, setCount] = useState(0);
update_details=kullanici.minebuildtime;
if ('undefined' !== typeof(update_details) && update_details !== "0") {
let d = new Date();
let currenttime = d.getTime();
timing_mat='';
timing_upgrade_value = '';
timing_mats_values = update_details.split("-");
[timing_cord_x,timing_cord_y,timing_mat,timing_upgrade_value,timing_time]= timing_mats_values;
let time_difference=timing_time-currenttime;
if(time_difference<0){
if(timing_upgrade_value ==="1"){
database.add(timing_cord_x,timing_cord_y,timing_mat,currentcordsandvalues,db,userUid,timing_time);
}else{
ctx.clearRect(timing_cord_x * 90, timing_cord_y * 120, 90, 120);
database.update(timing_cord_x,timing_cord_y,timing_mat,timing_upgrade_value,currentcordsandvalues,db,userUid,timing_time);
}
ctx_yazi.clearRect(timing_cord_x, timing_cord_y, 100, 30);
if(upgrade_status === false){
setStyleProp("backgroundColor", "transparent");
}
upgrade_status=true;
}else{
ctx_yazi.clearRect(timing_cord_x, timing_cord_y, 100, 30);
if(upgrade_status!==false){
//this is the part that doesnt work properly, only works if i use
//setStyleProp once
setStyleProp('position', 'absolute');
setStyleProp('backgroundColor', 'blue');
setStyleProp('left', timing_cord_x*90+30);
setStyleProp('top', timing_cord_y*120+150);
ctx_yazi.font = "14px Arial";
ctx_yazi.fillStyle = "black";
ctx_yazi.textAlign = "bottom";
}
ctx_yazi.fillText(Math.trunc(time_difference/1000), 5, 17);
upgrade_status=false;
}
}else{
upgrade_status=true;
}
return (
<>
{user ? (
<div className="screens">
<canvas
ref={ref2}
className="buildings"
width={800}
height={120}
onClick={e => {
}}
/>
<canvas
ref={ref}
className="gameScreen"
width={800}
height={600}
onClick={e => {
}}
/>
<canvas
ref={ref_yazi}
style={style}
width={30}
height={25}
/>
</div>
) : (<div>Please login</div>)}
</>
);
}
Okay after a bit struggle i managed get it work by doing a few changes. First of all i realised setStyleProp function under Economy function was changing only the last property of the style so i changed the function setStyleProp to accept 3 variables like below,
setStyleProp = (prop1, value1,prop2,value2,prop3,value3) => {
setStyle({
...style,
[prop1]: value1,
[prop2]: value2,
[prop3]: value3,
})
}
and then i changed the function call under Economy function like this;
setStyleProp('top', timing_cord_y*120+150,'left', timing_cord_x*90+30,'backgroundColor','white');
and it worked just like expected. The weird part i didnt understand i didnt change function calls at onClick events and they still work like below
setStyleProp('left', x_cord*90+30);
setStyleProp('top', y_cord*120+150);
setStyleProp('backgroundColor', "white");

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