const {useState, useEffect, useRef} = React;
const App = () => {
const [pressed, setPressed] = useState(false);
const [shoot, setShoot] = useState(false);
const [seconds, setSeconds] = useState(0);
useInterval(() => {
// Your custom logic here
pressed && seconds < 3 && setSeconds((prev)=> Number((prev+0.1).toFixed(1)));
}, 100);
useInterval(()=>{
!pressed && seconds > 0 && setSeconds((prev)=>{
if( Number((prev-0.5).toFixed(1)) < 0){
return 0;
}
return Number((prev-0.5).toFixed(1))
});
}, 20)
return (
<div>
<button
onMouseDown={()=>{
console.log('mouseDown')
setShoot(false);
setPressed(true);
}}
onMouseUp={()=>{
console.log('mouseUp')
setShoot(true);
setPressed(false);
}}
style={{
transform: `rotate(-${seconds*15}deg)`
}}
>Press</button>
<span className={`dot ${shoot ? '--shooted' : ''}`} />
<p>{seconds}</p>
</div>
)
};
ReactDOM.render(<App />, document.getElementById('root'));
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
.dot{
position: absolute;
width: 16px;
height: 16px;
border-radius:100%;
background: red;
}
.dot.--shooted{
animation: test 1s;
}
#keyframes test{
0%{
transform: translateX(0px);
}
100%{
transform: translateX(200px); // it should be dynamic px.
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.26.0/moment.min.js"></script>
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root" />
I'd like to move the red dot as much as the seconds I pressed the button.
but I am using animation so I can't control the px in CSS.
If I pressed the button for 3seconds, the red dot should be moved to 300px.
If I pressed the button for 1seconds, the red dot should be moved to 100px.
This is an example. But you need to add a logic to move it back.
const {
useState,
useEffect,
useRef
} = React;
const App = () => {
const [pressed, setPressed] = useState(false);
const [shoot, setShoot] = useState(false);
const [seconds, setSeconds] = useState(0);
const dotRef = useRef();
useInterval(() => {
// Your custom logic here
pressed && seconds < 3 && setSeconds((prev) => Number((prev + 0.1).toFixed(1)));
}, 100);
useInterval(() => {
!pressed && seconds > 0 && setSeconds((prev) => {
if (Number((prev - 0.5).toFixed(1)) < 0) {
return 0;
}
return Number((prev - 0.5).toFixed(1))
});
}, 20)
const handleMouseUp = () => {
dotRef.current.style.transform = `translateX(${seconds * 100}px)`;
}
return ( <
div >
<
button onMouseDown = {
() => {
console.log('mouseDown')
setShoot(false);
setPressed(true);
}
}
onMouseUp = {
() => {
console.log('mouseUp')
setShoot(true);
setPressed(false);
handleMouseUp();
}
}
style = {
{
transform: `rotate(-${seconds*15}deg)`
}
} >
Press < /button> <
span className = {
`dot ${shoot ? '--shooted' : ''}`
}
ref = {
dotRef
}
/> <
p > {
seconds
} < /p> < /
div >
)
};
ReactDOM.render( < App / > , document.getElementById('root'));
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
.dot {
position: absolute;
width: 16px;
height: 16px;
border-radius: 100%;
background: red;
transition: transform 1s;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.26.0/moment.min.js"></script>
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root" />
Related
I have a list of p tags, and I want to cycle through this list, by fading in one p tag, then fading out, then again fading in after replacing it.
Here is this codepen in jQuery: https://codepen.io/motion333/pen/EBBGVM
I am trying to do this in React by this:
useEffect(() => {
(function() {
var quotes = document.getElementsByClassName('tagline-text');
var quoteIndex = -1;
function showNextQuote() {
++quoteIndex;
document.querySelectorAll(".tagline-text")[quoteIndex % quotes.length].fadeIn(1000).delay(1000).fadeOut(1000, showNextQuote);
}
showNextQuote();
})();
}, []);
And this is the conainer:
<div className="tagline h-100 d-flex flex-column align-items-center justify-content-center">
<p className="tagline-text">Your Business</p>
<p className="tagline-text">Your Brand</p>
<p className="tagline-text">Your Content</p>
<p className="tagline-text">Your Portfolio</p>
<p className="tagline-text">You.</p>
</div>
But it gives me this error:
Uncaught TypeError: document.querySelectorAll(...)[(quoteIndex % quotes.length)].fadeIn is not a function
this should do it.
const { useState, useEffect } = React;
const texts = ["Your Business", "Your Brand", "Your Content", "Your Portfolio", "You."];
const time_between_text = 2; // text show for 2s before fade out.
const transition_duration = 0.5;
const App = () => {
const [show, setShow] = useState(0);
useEffect(() => {
const timerId = setInterval(() => {
setShow(p => {
if(p === texts.length - 1) p = -transition_duration;
else p = p + transition_duration;
return p;
});
}, time_between_text * 1000)
return () => clearInterval(timerId);
}, [])
return <div className="pContainer">
{texts.map((t, i) => <p key={i} style={{ opacity: `${show === i ? 1 : 0}`, transitionDuration: `${time_between_text + transition_duration}s` }}>{t}</p>)}
</div>
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<App />
);
.pContainer {
position: relative;
}
.pContainer p {
font-size: 36px;
font-weight: bold;
position: absolute;
top: 0;
left: 0;
opacity: 0;
transition-property: opacity;
transition-timing-function: ease-in-out;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
please help me! I'm using context API to pass the tracks to MucsicPlayer. but whenever I set setTracks state audio element src updates but audioRef.current doesn't update. I inspected it and saw that audioRef.current = <audio preload="auto" src(unknown)>. so ref src does not update. what should I do.
import React, { useState, useEffect, useRef, useContext } from 'react'
import { TrackContext } from '../../music/TrackContext'
const MusicPlayer = () => {
const [tracks, setTracks] = useContext(TrackContext)
console.log(tracks)
// states
const [trackIndex, setTrackIndex] = useState(0)
console.log(trackIndex)
const [trackProgress, setTrackProgress] = useState(0)
const [isPlaying, setIsPlaying] = useState(false)
// eslint-disable-next-line
const [volume, setVolume] = useState(1)
const { title, artist, image, audioSrc } = tracks[trackIndex]
//refs
const audio = new Audio(audioSrc)
const audioRef = useRef(audio)
const intervalRef = useRef()
const isReady = useRef(false)
console.log(audioRef.current)
const { duration } = audioRef.current
const toPrevTrack = () => {
if (trackIndex - 1 < 0) {
setTrackIndex(tracks.length - 1)
} else {
setTrackIndex(trackIndex - 1)
}
}
const toNextTrack = () => {
if (trackIndex < tracks.length - 1) {
setTrackIndex(trackIndex + 1)
} else {
setTrackIndex(0)
}
}
const startTimer = () => {
clearInterval(intervalRef.current)
intervalRef.current = setInterval(() => {
if (audioRef.current.ended) {
toNextTrack()
} else {
setTrackProgress(audioRef.current.currentTime);
}
}, [1000])
}
useEffect(() => {
if (isPlaying) {
audioRef.current.play()
startTimer();
} else {
clearInterval(intervalRef.current)
audioRef.current.pause()
}
// eslint-disable-next-line
}, [isPlaying])
useEffect(() => {
return () => {
audioRef.current.pause()
clearInterval(intervalRef.current)
}
}, [])
useEffect(() => {
audioRef.current.play()
audioRef.current = new Audio(audioSrc)
setTrackProgress(audioRef.current.currentTime)
if (isReady.current) {
audioRef.current.play()
setIsPlaying(true)
startTimer()
} else {
isReady.current = true
}
// eslint-disable-next-line
}, [trackIndex])
const onScrub = (value) => {
clearInterval(intervalRef.current)
audioRef.current.currentTime = value
setTrackProgress(audioRef.current.currentTime)
}
const onScrubEnd = () => {
if (!isPlaying) {
setIsPlaying(true);
}
startTimer();
}
const onScrubVolume = (value) => {
audioRef.current.volume = value
setVolume(audioRef.current.value)
}
function formatMinutes(sec) {
return new Date(sec * 1000).toUTCString().split(" ")[4].substr(3, 8)
}
const currentPercentage = duration ? `${(trackProgress / duration) * 100}%` : '0%';
const trackStyling = `-webkit-gradient(linear, 0% 0%, 100% 0%, color-stop(${currentPercentage}, #fff), color-stop(${currentPercentage}, #777))`;
return (
<div className="player">
<div className="left-block">
<div className="art">
<img src={image} alt="" />
</div>
<div className="song-details">
<div className="song-name">{title}</div>
<div className="artist-name">
{artist}
</div>
</div>
</div>
<div className="center-block">
<div className="song-progress">
<div>{formatMinutes(audioRef.current.currentTime)}</div>
<input
value={trackProgress}
step="1"
min="1"
max={duration ? duration : `${duration}`}
onChange={(e) => onScrub(e.target.value)}
onMouseUp={onScrubEnd}
onKeyUp={onScrubEnd}
style={{ background: trackStyling }}
type="range" />
<div>{duration ? formatMinutes(duration) : "00:00"}</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default MusicPlayer
useRef will not get re-initialized on every render.
So it will stay the same as it was initialized the very first time.
So whenever you are switching your track you have to update the audioRef too.
Change your toPrevTrack and toNextTrack
const toPrevTrack = () => {
const prevIndex = trackIndex - 1 < 0 ? tracks.length - 1 : trackIndex - 1;
const { audioSrc } = tracks[prevIndex]
audioRef.current = new Audio(audioSrc);
}
const toNextTrack = () => {
const nextIndex = trackIndex < tracks.length - 1 ? trackIndex + 1 : 0;
const { audioSrc } = tracks[nextIndex]
audioRef.current = new Audio(audioSrc);
}
Here a small demo. There are a few block; hovering on each block appears a tooltip(orange rect). It doesn't work correctly.
Tooltip should be displayed from left or right side. To get sizes of tooltip need to display it. Coords to display tooltip can be calculated only after tooltip is displayed
Codesandbox https://codesandbox.io/s/react-ref-65jj6?file=/src/index.js:88-231
const { useState, useEffect, useCallback } = React;
function App() {
return (
<div>
<HoveredBlock index={1} />
<HoveredBlock index={2} blockStyle={{ marginLeft: "5%" }} />
<HoveredBlock index={3} blockStyle={{ marginLeft: "50%" }} />
</div>
);
}
function calcCoords(blockRect, hoverRect) {
const docWidth = document.documentElement.clientWidth;
const isLeft = blockRect.right + hoverRect.width > docWidth;
const coords = {};
if (!isLeft) {
coords.x = blockRect.right;
coords.y = blockRect.top;
coords.type = "right";
} else {
coords.x = blockRect.left - 5 - hoverRect.width;
coords.y = blockRect.top;
coords.type = "left";
}
return coords;
}
function HoveredBlock({ index, blockStyle }) {
const [blockRect, setBlockRect] = useState();
const [hoverRect, setHoverRect] = useState();
const [showHover, setShowHover] = useState(false);
const [coords, setCoords] = useState();
const blockRef = useCallback((node) => {
if (node) {
setBlockRect(node.getBoundingClientRect());
}
}, []);
const hoverRef = useCallback(
(node) => {
if (showHover && node) {
setHoverRect(node.getBoundingClientRect());
}
},
[showHover]
);
useEffect(() => {
if (showHover && hoverRect) {
const coords = calcCoords(blockRect, hoverRect);
setCoords(coords);
}
}, [hoverRect]);
const isHidden = !showHover || !coords ? 'hidden' : '';
return (
<div>
<div
ref={blockRef}
className="block"
style={blockStyle}
onMouseEnter={() => setShowHover(true)}
onMouseLeave={() => setShowHover(false)}
>
{index}
</div>
<div
ref={hoverRef}
className={'hover-block' + isHidden}
style={{
left: coords && coords.x,
top: coords && coords.y
}}
/>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
.block {
width: 100px;
height: 100px;
background-color: aquamarine;
margin-left: 82%;
}
.hover-block {
position: fixed;
width: 100px;
height: 100px;
background-color: coral;
}
.hidden {
display: none;
}
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
I solved it. I changed the way how to hide element: visibility:hidded instead of display:none
export default function HoveredBlock({ blockStyle }) {
const [blockRect, setBlockRect] = useState();
const [hoverRect, setHoverRect] = useState();
const [showHover, setShowHover] = useState(false);
const [coords, setCoords] = useState();
const blockRef = useCallback((node) => {
if (node) {
setBlockRect(node.getBoundingClientRect());
}
}, []);
const hoverRef = useCallback((node) => {
if (node) {
setHoverRect(node.getBoundingClientRect());
}
}, []);
useEffect(() => {
if (showHover) {
console.log({ blockRect, hoverRect });
const coords = calcCoords(blockRect, hoverRect);
setCoords(coords);
}
}, [showHover, blockRect, hoverRect]);
return (
<>
<div
ref={blockRef}
className="block"
style={blockStyle}
onMouseEnter={() => setShowHover(true)}
onMouseLeave={() => setShowHover(false)}
/>
<div
ref={hoverRef}
className={cx("hover-block", {
hidden: !showHover || !coords
})}
style={{
left: coords && coords.x,
top: coords && coords.y
}}
></div>
</>
);
}
.block {
width: 100px;
height: 100px;
background-color: aquamarine;
margin-left: 20%;
}
.hover-block {
position: fixed;
width: 100px;
height: 100px;
background-color: coral;
}
.hidden {
visibility: hidden;
}
I am working on a progress bar (Eventually..) and I want to stop the animation (calling cancelAnimationRequest) when reaching a certain value (10, 100, ..., N) and reset it to 0.
However, with my current code, it resets to 0 but keeps running indefinitely. I think I might have something wrong in this part of the code:
setCount((prevCount) => {
console.log('requestRef.current', requestRef.current, prevCount);
if (prevCount < 10) return prevCount + deltaTime * 0.001;
// Trying to cancel the animation here and reset to 0:
cancelAnimationFrame(requestRef.current);
return 0;
});
This is the whole example:
const Counter = () => {
const [count, setCount] = React.useState(0);
// Use useRef for mutable variables that we want to persist
// without triggering a re-render on their change:
const requestRef = React.useRef();
const previousTimeRef = React.useRef();
const animate = (time) => {
if (previousTimeRef.current != undefined) {
const deltaTime = time - previousTimeRef.current;
// Pass on a function to the setter of the state
// to make sure we always have the latest state:
setCount((prevCount) => {
console.log('requestRef.current', requestRef.current, prevCount);
if (prevCount < 10) return prevCount + deltaTime * 0.001;
// Trying to cancel the animation here and reset to 0:
cancelAnimationFrame(requestRef.current);
return 0;
});
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
}
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []);
return <div>{ Math.round(count) }</div>;
}
ReactDOM.render(<Counter />, document.getElementById('app'));
html {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
body {
font-size: 60px;
font-weight: 700;
font-family: 'Roboto Mono', monospace;
color: #5D9199;
background-color: #A3E3ED;
}
.as-console-wrapper {
max-height: 66px !important;
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Code pen: https://codepen.io/fr-nevin/pen/RwrLmPd
The main problem with your code is that you are trying to cancel an update that has already been executed. Instead, you can just avoid requesting that last update that you don't need. You can see the problem and a simple solution for that below:
const Counter = () => {
const [count, setCount] = React.useState(0);
const requestRef = React.useRef();
const previousTimeRef = React.useRef(0);
const animate = React.useCallback((time) => {
console.log(' RUN:', requestRef.current);
setCount((prevCount) => {
const deltaTime = time - previousTimeRef.current;
const nextCount = prevCount + deltaTime * 0.001;
// We add 1 to the limit value to make sure the last valid value is
// also displayed for one whole "frame":
if (nextCount >= 11) {
console.log(' CANCEL:', requestRef.current, '(this won\'t work as inteneded)');
// This won't work:
// cancelAnimationFrame(requestRef.current);
// Instead, let's use this Ref to avoid calling `requestAnimationFrame` again:
requestRef.current = null;
}
return nextCount >= 11 ? 0 : nextCount;
});
// If we have already reached the limit value, don't call `requestAnimationFrame` again:
if (requestRef.current !== null) {
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
console.log('- SCHEDULE:', requestRef.current);
}
}, []);
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []);
// This floors the value:
// See https://stackoverflow.com/questions/7487977/using-bitwise-or-0-to-floor-a-number.
return (<div>{ count | 0 } / 10</div>);
};
ReactDOM.render(<Counter />, document.getElementById('app'));
html {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
body {
font-size: 60px;
font-weight: 700;
font-family: 'Roboto Mono', monospace;
color: #5D9199;
background-color: #A3E3ED;
}
.as-console-wrapper {
max-height: 66px !important;
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
In any case, you are also updating the state many more times than actually needed, which you can avoid by using refs and the timestamp (time) provided by requestAnimationFrame to keep track of the current and next/target counter values. You are still going to call the requestAnimationFrame update function the same number of times, but you will only update the state (setCount(...)) once you know the change is going to be reflected in the UI.
const Counter = ({ max = 10, rate = 0.001, location }) => {
const limit = max + 1;
const [count, setCount] = React.useState(0);
const t0Ref = React.useRef(Date.now());
const requestRef = React.useRef();
const targetValueRef = React.useRef(1);
const animate = React.useCallback(() => {
// No need to keep track of the previous time, store initial time instead. Note we can't
// use the time param provided by requestAnimationFrame to the callback, as that one won't
// be reset when the `location` changes:
const time = Date.now() - t0Ref.current;
const nextValue = time * rate;
if (nextValue >= limit) {
console.log('Reset to 0');
setCount(0);
return;
}
const targetValue = targetValueRef.current;
if (nextValue >= targetValue) {
console.log(`Update ${ targetValue - 1 } -> ${ nextValue | 0 }`);
setCount(targetValue);
targetValueRef.current = targetValue + 1;
}
requestRef.current = requestAnimationFrame(animate);
}, []);
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []);
React.useEffect(() => {
// Reset counter if `location` changes, but there's no need to call `cancelAnimationFrame` .
setCount(0);
t0Ref.current = Date.now();
targetValueRef.current = 1;
}, [location]);
return (<div className="counter">{ count } / { max }</div>);
};
const App = () => {
const [fakeLocation, setFakeLocation] = React.useState('/');
const handleButtonClicked = React.useCallback(() => {
setFakeLocation(`/${ Math.random().toString(36).slice(2) }`);
}, []);
return (<div>
<span className="location">Fake Location: { fakeLocation }</span>
<Counter max={ 10 } location={ fakeLocation } />
<button className="button" onClick={ handleButtonClicked }>Update Parent</button>
</div>);
};
ReactDOM.render(<App />, document.getElementById('app'));
html {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
body {
font-family: 'Roboto Mono', monospace;
color: #5D9199;
background-color: #A3E3ED;
}
.location {
font-size: 16px;
}
.counter {
font-size: 60px;
font-weight: 700;
}
.button {
border: 2px solid #5D9199;
padding: 8px;
margin: 0;
font-family: 'Roboto Mono', monospace;
color: #5D9199;
background: transparent;
outline: none;
}
.as-console-wrapper {
max-height: 66px !important;
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
I have literally tried for a few hours to replicate a clickable ticker, much like they have at the very top of this site: https://www.thebay.com/
I'm confused about what triggers useEffect and long story short, I can't come up with a solution that keeps the ticker moving AND also gives the option of clicking forward/backwards via arrows. Clicking the arrow should not permanently pause the ticker.
function Ticker() {
const [tickerDisplay, setTickerDisplay] = useState('Free In-store Pickup')
const [tickerIndex, setTickerIndex] = useState(0)
const [arrowClicked, setArrowClicked] = useState(false)
const notices = [
'Easy Returns within 30 Days of Purchase',
'Free Shipping on $99+ Orders',
'Free In-store Pickup',
]
const handleClick = (side) => {
setArrowClicked(true)
switch (side) {
case 'left':
setTickerIndex(
tickerIndex === 0 ? notices.length - 1 : tickerIndex - 1
)
break
case 'right':
setTickerIndex(
tickerIndex === notices.length - 1 ? 0 : tickerIndex + 1
)
break
default:
console.log('something went wrong')
break
}
}
useEffect(() => {
if (arrowClicked) {
setTickerDisplay(notices[tickerIndex])
setTickerIndex(
tickerIndex === notices.length - 1 ? 0 : tickerIndex + 1
)
setArrowClicked(false)
return
}
setTimeout(() => {
setTickerDisplay(notices[tickerIndex])
setTickerIndex(
tickerIndex === notices.length - 1 ? 0 : tickerIndex + 1
)
console.log('This will run every 6 seconds!')
}, 6000)
}, [tickerIndex, notices, tickerDisplay, arrowClicked])
return (
<IconContext.Provider value={{ className: 'ticker-icons-provider' }}>
<div className='ticker'>
<FaChevronLeft onClick={() => handleClick('left')} />
<div className='ticker_msg-wrapper'>{tickerDisplay}</div>
<FaChevronRight onClick={() => handleClick('right')} />
</div>
</IconContext.Provider>
)
}
export default Ticker
What is the best way to code this component?
This is not a work of art and probably some things could've been done better.
Hope that suits you.
const { useRef, useState, useEffect } = React;
const getItems = () => Promise.resolve(['All of our questions are now open', 'Answers extended: 72 hours after questions open', 'Post a question or get an answer', 'Free badges on 20k points'])
const Ticker = ({onPrevious, onNext, items, currentIndex}) => {
const ref = useRef(null);
const [size, setSize] = useState({
width: 0,
widthPx: '0px',
height: 0,
heightPx: '0px'
})
useEffect(() => {
if(ref && ref.current) {
const {width, height} = ref.current.getBoundingClientRect();
setSize({
width,
widthPx: `${width}px`,
height,
height: `${height}px`
})
}
}, [ref]);
const calculateStyleForItem = (index) => {
return {
width: size.width,
transform: `translateX(${0}px)`
}
}
const calculateStyleForContainer = () => {
return {
width: `${size.width * (items.length + 1)}px`,
transform: `translateX(${-currentIndex * size.width + 2 * size.width}px)`
}
}
return <div ref={ref} className="ticker">
<div style={{width: size.widthPx, height: size.heightPx}} className="ticker__foreground">
<div onClick={onPrevious} className="arrow">{'<'}</div>
<div onClick={onNext} className="arrow">{'>'}</div>
</div>
<div>
<div style={calculateStyleForContainer()} className="ticker__values">
{items.map((value, index) => <div key={index} style={calculateStyleForItem(index)}className="ticker__value">{value}</div>)}
</div>
</div>
</div>
}
const App = () => {
const [items, setItems] = useState([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [clicked, setClicked] = useState(false);
useEffect(() => {
let isUnmounted = false;
getItems()
.then(items => {
if(isUnmounted) {
return
}
setItems(items);
})
return () => {
isUnmounted = true;
}
}, [])
useEffect(() => {
if(!items.length) {
return () => {
}
}
let handle = null;
const loop = () => {
if(!clicked) {
onNext(null);
}
setClicked(false);
handle = setTimeout(loop, 2000);
}
handle = setTimeout(loop, 2000);
return () => {
clearTimeout(handle);
}
}, [items, clicked])
const onPrevious = () => {
setClicked(true);
setCurrentIndex(index => (index - 1) > -1 ? index - 1 : items.length - 1)
}
const onNext = (programmatically) => {
if(programmatically) {
setClicked(programmatically);
}
setCurrentIndex(index => (index + 1) % items.length)
}
return <div>
{items.length ? <Ticker onPrevious={onPrevious} onNext={onNext} currentIndex={currentIndex} items={items}/> : 'Loading'}
</div>
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
html, body {
box-sizing: border-box;
margin: 0;
}
.ticker {
display: flex;
justify-content: center;
align-items: center;
background: black;
font-size: 1rem;
color: white;
font-weight: bold;
padding: 1rem;
overflow: hidden;
}
.ticker__foreground {
position: absolute;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.ticker__values {
transition: all .3s ease-in;
}
.ticker__value {
text-align: center;
display: inline-block;
vertical-align: middle;
float: none;
}
.arrow {
font-size: 1.5rem;
cursor: pointer;
padding: 1rem;
}
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>