I'm having a problem with inline style changes that are reset when my dispatch is finished, because the state is being re-rendered, despite the other functionality of my component is working (you can still see that the counter is not stopping).
Here's a demonstration of what I mean.
You can see that the orange bar of the left box vanishes when the orange bar of the right bar finishes (the animation ends). Essentially what I'm doing here is changing the width property in inline styles.
import React, { useEffect, useRef } from "react";
import { useDispatch, connect } from "react-redux";
import { addProfessionExperience } from "../../actions/index";
import "./Professions.sass";
const timers = [];
const progressWidths = [];
const mapStateToProps = (state, ownProps) => {
const filterID = +ownProps.match.params.filter;
const { professions, professionExperience } = state;
return {
professions: professions.find(item => item.id === filterID),
professionExperience: professionExperience
};
};
const produceResource = (dispatch, profession, sub, subRef) => {
if(timers[sub.id]) return;
/*
* Begin the progress bar animation/width-change.
*/
Object.assign(subRef.current[sub.id].style, {
width: "100%",
transitionDuration: `${sub.duration}s`
});
/*
* Updates the progress text with the remaining time left until done.
*/
let timeLeft = sub.duration;
const timeLeftCountdown = _ => {
timeLeft--;
timeLeft > 0 ? setTimeout(timeLeftCountdown, 1000) : timeLeft = sub.duration;
subRef.current[sub.id].parentElement.setAttribute("data-duration", timeLeft + "s");
}
setTimeout(timeLeftCountdown, 1000);
/*
* Dispatch the added experience from profession ID and sub-profession level.
* We do not allow duplicate timers, only one can be run at a time.
*/
const timer = setTimeout(() => {
Object.assign(subRef.current[sub.id].style, {
width: "0%",
transitionDuration: "0.2s"
});
dispatch(addProfessionExperience({ id: profession.id, level: sub.level }));
delete timers[sub.id];
}, sub.duration * 1000);
timers[sub.id] = timer;
};
const isSubUnlocked = (professionMaxExperience, subLevel, professionExperience) => {
if(professionExperience <= 0 && subLevel > 1) return false;
return professionExperience >= getExperienceThreshold(professionMaxExperience, subLevel);
};
const getExperienceThreshold = (professionMaxExperience, subLevel) => (((subLevel - 1) * 1) * (professionMaxExperience / 10) * subLevel);
const ConnectedList = ({ professions, professionExperience }) => {
const currentExperience = professionExperience.find(item => item.profession === professions.id);
const subRef = useRef([]);
const dispatch = useDispatch();
useEffect(() => {
subRef.current = subRef.current.slice(0, professions.subProfessions.length);
}, [professions.subProfessions]);
return (
<div>
<div className="list">
<ul>
{professions.subProfessions.map(el => {
const unlocked = isSubUnlocked(
professions.maxExperience,
el.level,
(currentExperience ? currentExperience.amount : 0)
);
const remainingExperience = getExperienceThreshold(professions.maxExperience, el.level) - (currentExperience ? currentExperience.amount : 0);
return (
<li
key={Math.random()}
style={{ "opacity": unlocked ? "1" : "0.5" }}
>
<div className="sprite">
<img alt="" src={`/images/professions/${el.image}.png`} />
</div>
<div className="caption">{el.name}</div>
<div
className="progress-bar"
data-duration={unlocked ? `${el.duration}s` : `${remainingExperience} XP to Unlock`}
data-identifier={el.id}
>
<span ref={r => subRef.current[el.id] = r} ></span>
</div>
<div className="footer">
<button
className="btn"
onClick={() => unlocked ? produceResource(dispatch, professions, el, subRef) : false}
>
{unlocked ?
`Click` :
<i className="fa fa-lock"></i>
}
</button>
</div>
</li>
);
})}
</ul>
</div>
</div>
);
};
const List = connect(mapStateToProps)(ConnectedList);
export default List;
How can I make it so the orange bars persist on their own and not disappears when another one finishes?
One problem is that you're using Math.random() to generate your keys. Keys are what the virtual DOM uses to determine whether an element is the "same" as the one on a previous render. By using a random key, you're telling the virtual DOM that you want to spit out a brand new DOM element instead of reusing the prior one, which means the new one won't retain any of the side effects you placed on the original element. Read up on React's reconciliation for more info on this.
Try to use keys that logically represent the thing you're rendering. In the case of your code, el.id looks like it may be a unique identifier for the subprofession you're rendering. Use that for the key instead of Math.random().
Additionally, refs are going to make reasoning about your code really difficult. Rather than using refs to manipulate your DOM, use state manipulation and prop passing, and let React re-render your elements with the new attributes.
Related
I want to change the source of image onscroll in reactjs. Like if scrollY is greater than 100 change the image source and if it is greater than 200 change it another source.
i tried to do it but could not. any ideas?
import React, { useEffect, useState, useRef } from 'react';
import './Video.css';
import { useInView } from 'react-intersection-observer';
function Video() {
const videoSrc1 = "https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/6341303c29c5340961dc9ae6_Mco-1-transcode.mp4";
const videoSrc2 = "https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63413ff244f1dc616b7148a0_Mco-transcode.mp4";
const videoSrc3 = "https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63455a67996ba248148c4e31_add-options%20(3)-transcode.mp4";
const img1 = 'https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63455a67996ba248148c4e31_add-options%20(3)-poster-00001.jpg';
const img2 = 'https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63413ff244f1dc616b7148a0_Mco-poster-00001.jpg';
const img3 = 'https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63455a67996ba248148c4e31_add-options%20(3)-poster-00001.jpg';
const [scrollPosition, setScrollPosition] = useState(0);
const handleScroll = () => {
const position = window.pageYOffset;
setScrollPosition(position);
};
useEffect(() => {
window.addEventListener('scroll', handleScroll, { passive: true })
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
{
if (scrollPosition>=316){
// this.src={videoSrc2}
}
}
console.log("position;", scrollPosition)
return (
<div className='container'>
<video loop autoPlay muted className='video'>
<source src={videoSrc1} type="video/webm" />
</video>
</div>
)
}
export default Video
You can do a combination of Vanilla JS methods and React methods to achieve this.
Your best best bet is to use the useEffect hook and add an event listener to the Window DOM object based on where on the page the scroll is position.
First you need a function that executes every time the DOM re-renders (scrolling does this)
start by using the useEffect hook
useEffect(() => {}, [])
Next you want a function that executes specifically when you scroll the page
you can add an event handler to the window DOM element
window.addEventListener('scroll',() => {})
Then you want to track the where you are on the page (how far up or how far down)
You can use the window's scrollTop property to return how far up or down you are on the page relative to the top of the page
document.documentElement.scrollTop
Now comes the logic part, you said you want to change the image's src based on how far up or down you've scrolled on the page
This is where, useState, boolean flags and the ternary operator come into play
You can write a useState hook to store the Y position of the scroll, and the useEffect and scroll event listener will keep updating it to the current position
const [scrollPosition, getScrollPositon] = useState(document.documentElement.scrollTop)
finally nest the hook function into the window 'scroll' function and nest that in the useEffect hook
const [scrollPosition, getScrollPositon] = useState(document.documentElement.scrollTop)
useEffect(() => {
window.addEventListener('scroll',() => {
getScrollPositon(document.documentElement.scrollTop);
})
}, [])
AND finally write the logic in your .jsx code to say 'when we are x number of pixel below the top of the screen...change the image source'
const App = () => {
return (
<div className='app'>
<img src={scrollPosition < 1000 ? 'http://imagelinkA.com' : 'http://imagelinkB.com'}>
</div>
);
}
Now you put it all together...
// App.js/jsx
import { useState, useEffect } from 'react';
const App = () => {
// initial scroll positon on page load
const [scrollPosition, getScrollPositon] = useState(document.documentElement.scrollTop)
// hook and event handlers to keep track of and update scroll
useEffect(() => {
window.addEventListener('scroll',() => {
getScrollPositon(document.documentElement.scrollTop);
})
}, [])
// your .jsx code with appropriate boolean flags and use of the ternary operator
return (
<div className='app'>
<img src={scrollPosition < 1000 ? 'http://imagelinkA.com' : 'http://imagelinkB.com'}>
</div>
);
}
Hope I was able to help!
Setting the scroll position will trigger needless rerenders, instead you only want to trigger a rerender when the data source will change.
To select the proper data source, putting the list of data sources in a list is a good way to do this. Then you can properly determine the index of data source to show with something like this:
// Y_OFFSET_DIFFERENCE is the value that determines when the next image should be shown.
const index =
Math.floor(position / Y_OFFSET_DIFFERENCE) % dataSources.length;
You can see how this is properly calculated:
If position = 0 and Y_OFFSET_DIFFERENCE = 100 then 0/100 = 0 and 0 % 2 is 0. 0 is the index of the first element of your list.
If position = 100 and Y_OFFSET_DIFFERENCE = 100 then 100/100 = 1 and 1 % 2 is 1. 1 is the index of the second element in your list.
If position = 150 and Y_OFFSET_DIFFERENCE = 100 then 150/100 = 1.5 and Math.floor(1.5) = 1 and 1 % 2 is 1. 1 is the index of the second element in your list.
If position = 200 and Y_OFFSET_DIFFERENCE = 100 then 200/100 = 2 and 2 % 2 is 0. 0 is the index of the first element in your list.
And it'll continue like this forever.
Here is the full code.
import { useState, useEffect } from "react";
const dataSources = [
"https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63455a67996ba248148c4e31_add-options%20(3)-poster-00001.jpg",
"https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63413ff244f1dc616b7148a0_Mco-poster-00001.jpg"
];
const DEFAULT_DATA_SOURCE = dataSources[0];
const Y_OFFSET_DIFFERENCE = 100;
export default function App() {
const [dataSource, setDataSource] = useState(DEFAULT_DATA_SOURCE);
useEffect(() => {
const handleScroll = () => {
const position = window.pageYOffset;
const index =
Math.floor(position / Y_OFFSET_DIFFERENCE) % dataSources.length;
const selectedSource = dataSources[index];
if (selectedSource === dataSource) {
return;
}
setDataSource(selectedSource);
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [dataSource]);
return (
<div
style={{ height: 2000, backgroundImage: "linear-gradient(blue, green)" }}
>
<div
style={{
position: "sticky",
top: 10,
left: 10,
display: "flex",
justifyContent: "center",
flexDirection: "column",
alignItems: "center"
}}
>
<p style={{ color: "white", textAlign: "center" }}>{dataSource}</p>
<img
src={dataSource}
alt="currently selected source"
width={100}
height={100}
/>
</div>
</div>
);
}
codesandbox demo
I created a filter gallery. I want to animate the filter items every time I click to a buttons. But my codes are not doing it properly. It animates filter items like toggle. If I click on a button first time it animates items, then If I click on another button it shows nothing. After that If I click on another button it animates again. What's wrong with my code? Experts please help me to find out the proper solution. Thanks in advance.
Here is my code:
import React, { useState } from 'react';
import suggestData from '../data/suggest-data.json';
const allCategories = ['All', ...new Set(suggestData.map(item => item.area))];
const Suggest = () => {
const [suggestItem, setSuggestItem] = useState(suggestData);
const [butto, setButto] = useState(allCategories);
const [selectedIndex, setSelectedIndex] = useState(0);
const [anim, setAnim] = useState(false);
const filter = (button) => {
if (button === 'All') {
setSuggestItem(suggestData);
return;
}
const filteredData = suggestData.filter(item => item.area === button);
setSuggestItem(filteredData);
}
const handleAnim = () => {
setAnim(anim => !anim);
}
return (
<div>
<h1>Suggest</h1>
<div className="fil">
<div className="fil-btns">
<div className="fil-btn">
<button className='btn'>Hello</button>
{
butto.map((cat, index) => {
return <button type="button" key={index} onClick={() => { filter(cat); setSelectedIndex(index); handleAnim(); }} className={"btn" + (selectedIndex === index ? " btn-active" : "")}>{cat}</button>
})
}
</div>
</div>
<div className="fil-items">
{
suggestItem.map((item, index) => {
return (
<div className={"fil-item" + (anim ? " fil-item-active" : "")} key={index}>
<h1>{item.name}</h1>
<h2>{item.category}</h2>
<h3>{item.location}</h3>
<h4>{item.type}</h4>
<h5>{item.area}</h5>
</div>
);
})
}
</div>
</div>
</div>
);
}
export default Suggest;
In your handleAnim() function, you are simply toggling the value of anim state. So initially, its value is false and when you click the button for the first time, it is set to true. On clicking the next button, the anim state becomes false because the value of !true is false and hence your animation doesn't work. On next click, becomes true again since !false is true and the toggle continues again and again.
If you want to make your animations work on every click you will need to set the anim state value to true on every button click as below since you seem to depend on the value to set animations. As an alternative, I think it will do just fine if you simply add the animation directly to the enclosing div with class .filter-item instead of relying on states to trigger the animation since after every filter you apply, the elements will be re-rendered and the animation will happen after every re-render.
const handleAnim = () => {
setAnim(true);
}
I'm building a web app with React that generates random movie quotes...
The problem arises when the quote is too long and it overflows outside the parent div...
I've tried altering the css with display flex and flex-wrap set to wrap. It does't work.
Here is my code.
import React from 'react';
import Typed from 'typed.js';
import './App.css';
import quotes from './quotes.json';
const random_quote = () => {
const rand = Math.floor(Math.random() * quotes.length);
let selected_quote = quotes[rand].quote + ' - ' + quotes[rand].movie;
return selected_quote;
}
const TypedQuote = () => {
// Create reference to store the DOM element containing the animation
const el = React.useRef(null);
// Create reference to store the Typed instance itself
const typed = React.useRef(null);
React.useEffect(() => {
const options = {
strings: [
random_quote(),
],
typeSpeed: 30,
backSpeed: 50,
};
// elRef refers to the <span> rendered below
typed.current = new Typed(el.current, options);
return () => {
// Make sure to destroy Typed instance during cleanup
// to prevent memory leaks
typed.current.destroy();
}
}, [])
return (
<div className="type-wrap">
<span style={{ whiteSpace: 'pre' }} ref={el} />
</div>
);
}
const App = () => {
return (
<>
<div className='background' id='background'>
<div className='quote-box'>
<TypedQuote />
</div>
<button onClick={random_quote}>New Quote</button>
</div>
</>
);
}
export default App;
I have this idea where I could implement a function that adds '\n' after like 10 words or like maybe after a '.' or ',' (I could implement some logic here). But this seems like a longshot. Is there a fancier way to do this?? Any help would be appreciated.
Try the property below on the parent container.
word-wrap: break-word;
or the below if you want to break words as well
word-break: break-all;
I have a function which creates 2 divs when changing the number of items correspondingly (say if we choose 5 TVs we will get 5 divs for choosing options). They serve to make a choice - only one of two possible options should be chosen for every TV, so when we click on one of them, it should change its border and background color.
Now I want to create a dynamic stylization for these divs: when we click on them, they should get a new class (tv-option-active) to change their styles.
For that purposes I used 2 arrays (classesLess and classesOver), and every time we click on one of divs we should remove a class if it's already applied to the opposite option and push the class to the target element - thus only one of options will have tv-option-active class.
But when I click on a div I do not get anything - when I open the document in the browser and inspect the elements, the elements do not even receive new class on click - however, when I console log the classes variable that should apply to an element, it is the way it should be - "less tv-option-active" or "over tv-option-active". I applied join method to remove the comma.
I checked the name of imported css file and it is ok so the problem is not there, also I applied some rules just to make sure the problem is not there and it worked when it's not dynamic (I mean no clicks are needed).
So my list of reasons causing that trouble seems to be not working.
I also tried to reorganize the code in order to not call a function in render return - putting mapping directly to render return, but this also didn't work.
Can anyone give me a hint why it is that?
Here is my code.
import React from 'react'
import { NavLink, withRouter } from 'react-router-dom'
import './TVMonting.css'
import PageTitle from '../../PageTitle/PageTitle'
class TVMontingRender extends React.Component {
state = {
tvData: {
tvs: 1,
under: 0,
over: 0,
},
}
render() {
let classesLess = ['less']
let classesOver = ['over']
const tvHandlers = {
tvs: {
decrease: () => {
if (this.state.tvData.tvs > 1) {
let tvData = {
...this.state.tvData,
}
tvData.tvs -= 1
this.setState({ tvData })
}
},
increase: () => {
if (this.state.tvData.tvs < 5) {
let tvData = {
...this.state.tvData,
}
tvData.tvs += 1
this.setState({ tvData })
}
},
},
}
const createDivs = () => {
const divsNumber = this.state.tvData.tvs
let divsArray = []
for (let i = 0; i < divsNumber; i++) {
divsArray.push(i)
}
return divsArray.map((i) => {
return (
<React.Fragment key={i}>
<div
className={classesLess.join(
' '
)}
onClick={() => {
const idx = classesOver.indexOf(
'tv-option-active'
)
if (idx !== -1) {
classesLess.splice(
idx,
1
)
}
classesLess.push(
'tv-option-active'
)
}}
>
Under 65
</div>
<div
className={classesOver.join(
' '
)}
onClick={() => {
const idx = classesLess.indexOf(
'tv-option-active'
)
if (idx !== -1) {
classesOver.splice(
idx,
1
)
}
classesOver.push(
'tv-option-active'
)
// classesOver.join(' ')
}}
>
Over 65
</div>
</React.Fragment>
)
})
}
return (
<div>
<button onClick={tvHandlers.tvs.decrease}>
-
</button>
{this.state.tvData.tvs === 1 ? (
<h1> {this.state.tvData.tvs} TV </h1>
) : (
<h1> {this.state.tvData.tvs} TVs </h1>
)}
<button onClick={tvHandlers.tvs.increase}>
+
</button>
{createDivs()}
</div>
)
}
}
export default withRouter(TVMontingRender)
CSS file is very simple - it just adds a border.
P.S. I know that with this architecture when I click on one of the divs all the divs will get tv-option-active class, but for now I just want to make sure that this architecture works - since I'm relatively new in React 🙂Thanks in advance!
Components won't have their lifecycle triggered if you are mutating a variable. You need a state for that purpose, which stores the handled data.
In your case you need some state to say which div has the active class, under or over. You can also abstract each rendered Tv to another Class component. This way you achieve independent elements that control their own class, rather than changing all others.
For that I created a Tv class, where I simplified some of the logic:
class Tv extends React.Component {
state = {
activeGroup: null
}
// this will update which group is active
changeActiveGroup = (activeGroup) => this.setState({activeGroup})
// activeClass will return 'tv-option-active' if that group is active
activeClass = (group) => (group === this.state.activeGroup ? 'tv-option-active' : '')
render () {
return (
<React.Fragment>
<div
className={`less ${ activeClass('under') }`}
onClick={() => changeActiveGroup('under')}
>
Under 65
</div>
<div
className={`over ${ activeClass('over') }`}
onClick={() => changeActiveGroup('over')}
>
Over 65
</div>
</React.Fragment>
)
}
}
Your TvMontingRender will be cleaner, also it's better to declare your methods at your class body rather than inside of render function:
class TVMontingRender extends React.Component {
state = {
tvData: {
tvs: 1,
under: 0,
over: 0,
}
}
decreaseTvs = () => {
if (this.state.tvData.tvs > 1) {
let tvData = {
...this.state.tvData,
}
tvData.tvs -= 1
this.setState({ tvData })
}
}
increaseTvs = () => {
if (this.state.tvData.tvs < 5) {
let tvData = {
...this.state.tvData,
}
tvData.tvs += 1
this.setState({ tvData })
}
}
createDivs = () => {
const divsNumber = this.state.tvData.tvs
let divsArray = []
for (let i = 0; i < divsNumber; i++) {
divsArray.push(i)
}
// it would be better that key would have an unique generated id (you could use uuid lib for that)
return divsArray.map((i) => <Tv key={i} />)
}
render() {
return (
<div>
<button onClick={this.decreaseTvs}>
-
</button>
{this.state.tvData.tvs === 1 ? (
<h1> {this.state.tvData.tvs} TV </h1>
) : (
<h1> {this.state.tvData.tvs} TVs </h1>
)}
<button onClick={this.increaseTvs}>
+
</button>
{this.createDivs()}
</div>
)
}
}
Note: I didn't change the key you are passing to Tv, but when handling an array that you manipulate somehow it's often better to pass an unique id identifier. There are some libs for that like uuid, nanoID.
When handling complex class logic, you may consider using libs like classnames, that would make your life easier.
I have a React.useRef in the parent component const portfolioRef = React.useRef(null) And want to use the current version of that in the child component const parent = portfolioRef.current. But when I use it in this way, it says cannot read property of current. Has anyone an idea how to fix this?
export function Portfolio() {
const portfolioRef = React.useRef(null)
return (
<div className={cx(styles.component, styles.scrollWrapper)}>
<div className={styles.topIcon} dangerouslySetInnerHTML={{ __html: arrow }} />
<div ref={portfolioRef} className={styles.scroll}>
<PortfolioItem
title='Article about Kaliber Academie'
text='I wrote an article about my experience at Kaliber'
link='https://medium.com/kaliberinteractive/hoe-technologie-het-hart-van-een-luie-scholier-veranderde-3cd3795c6e33'
linkTekst='See Article' />
</div>
</div>
)
}
function PortfolioItem({ text, title, link, linkTekst, portfolioRef }) {
const portfolioItemRef = React.useRef(null)
React.useEffect(() => {
const element = portfolioItemRef.current
const parent = portfolioRef.current
calculateDistance(parent, element)
}, [portfolioRef])
return (
<div ref={portfolioItemRef} className={styles.componentItem}>
<div className={styles.title}>{title}</div>
<div className={styles.content}>
<div className={styles.text}>{text}</div>
<div className={styles.links}>
<a className={styles.linkTekst} href={link}>{linkTekst} </a>
<div className={styles.linkIcon} dangerouslySetInnerHTML={{
__html:arrow }} />
</div>
</div>
</div>
)
}
function calculateDistance(parent, element) {
const parentRect = parent.getBoundingClientRect()
const parentCenter = parentRect.top + parentRect.height / 2
const elementRect = element.getBoundingClientRect()
const elementCenter = elementRect.top + elementRect.height / 2
const distance = Math.abs(elementCenter - parentCenter)
console.log(distance)
}
It seems that you don't pass the ref to the child at all. Try to pass it in props
<PortfolioItem
portfolioRef={portfolioRef} {/* here you are passing the ref in props */}
title="Article about Kaliber Academie"
text="I wrote an article about my experience at Kaliber"
link="https://medium.com/kaliberinteractive/hoe-technologie-het-hart-van-een-luie-scholier-veranderde-3cd3795c6e33"
linkTekst="See Article"
/>;
function PortfolioItem({ text, title, link, linkTekst, portfolioRef }) {
const portfolioItemRef = React.useRef(null) //line 1
const element = portfolioItemRef.current //line 2
const parent = portfolioRef.current //line 3
calculateDistance(parent, element)
return (
<div ref={portfolioItemRef} className={styles.componentItem}>
</div>
)
}
when the code execute in order
Line #1: create a reference to hold an element or value by using useRef nad initiate it to null,
Line #2: trying to access current value of the ref which is still null //Error, because ref is not holding any value at this moment
to access the value you have to put the logic inside useRef
React.useEffect(() => {
const element = portfolioItemRef.current
const parent = portfolioRef.current
calculateDistance(parent, element)
}, [])
not tested.