useState acting out when trying to update with a setTimeout - javascript

I am trying to use setTimeout to flip a useState between true and false every 6 seconds. I use this state to add/ remove a class from a div - this will cause the div to toggle between top: 0 to top: 100% (and a transition takes care of the animation).
To be more specific, I have an iPhone wrapper image with a content image inside it. The idea is that it will slowly scroll to the bottom of the content image, and then after 6 seconds (from the time it started scrolling down), it will then start scrolling back up, ad infinitum. However, it's not working right at all.
I've tested it with an onClick and it works exactly as I intend. However, with the setTimeout logic:
The state is always false - even if we set it to true and then log it
The state is always true from the perspective of the JSX - the class is always added, which implies that the state is true
Of course, it cannot be true and false, and it should be flipping its value. Could someone tell me why it's not working and perhaps tell me why it's acting in this bizarre way?
import React, { useState } from 'react';
import iphone from '../Images/iphone.png'
const Iphone = (props) => {
const [isInitialised, setInitialised] = useState(false)
const [animating, setAnimating] = useState(false)
const startAnimation = () => {
setAnimating(!animating); /* Even if I use `true`, it will log to the console as `false` */
console.warn('animation change iphone!');
console.warn(animating);
console.warn(isInitialised); /* This also always logs as `false` */
setTimeout(() => {
startAnimation();
}, 6000);
}
if (!isInitialised) {
setInitialised(true);
startAnimation();
}
return (
<div className={`iphone align-mobile-center ${animating ? "iphone--animating" : ""}`} onClick={() => setAnimating(!animating)}>
<img className="iphone__image" src={iphone} alt="An iPhone" />
<div className="iphone__content">
<img className="iphone__content-image" src={props.image} alt={props.alt} />
</div>
</div>
)
}
export default Iphone;
I use the isInitialised otherwise it seems to hit an infinite loop.

You can do something like this.
useEffect with empty array as deps so it will only once, you don't need isInitialized state.
In useEffect use setInterval so it will run every 6 seconds.
use callback way to setting state so you always get the correct value of animating.
import React, { useState } from 'react';
import iphone from '../Images/iphone.png'
const Iphone = (props) => {
const [animating, setAnimating] = useState(false)
useEffect(() => {
const startAnimation = () => {
setAnimating(v => !v);
}
const interval = setInterval(() => {
startAnimation();
}, 6000);
return () => clearInterval(interval);
}, []);
return (
<div className={`iphone align-mobile-center ${animating ? "iphone--animating" : ""}`} onClick={() => setAnimating(!animating)}>
<img className="iphone__image" src={iphone} alt="An iPhone" />
<div className="iphone__content">
<img className="iphone__content-image" src={props.image} alt={props.alt} />
</div>
</div>
)
}
export default Iphone;

you can use useEffect and bring setTimeout outside.
import React, { useState } from 'react';
import iphone from '../Images/iphone.png'
const Iphone = (props) => {
const [isInitialised, setInitialised] = useState(false)
const [animating, setAnimating] = useState(false)
useEffect(()=>{
startAnimation();
},[]);
const startAnimation = () => {
setAnimating(!animating);
console.warn('animation change iphone!');
console.warn(animating);
console.warn(isInitialised);
}
setTimeout(() => {
if (!isInitialised) {
setInitialised(true);
startAnimation();
}
}, 6000);
return (
<div className={`iphone align-mobile-center ${animating ? "iphone--animating" : ""}`} onClick={() => setAnimating(!animating)}>
<img className="iphone__image" src={iphone} alt="An iPhone" />
<div className="iphone__content">
<img className="iphone__content-image" src={props.image} alt={props.alt} />
</div>
</div>
)
}
export default Iphone;

Related

How to make element scrolled to bottom in React.js?

In my app I want to make my element always scrolled to bottom after getting new logs.
For some reason my logsRef.current.scrollTop has value of zero all the time. My logs do show on screen and in console. I am not sure why is this not working, I've tried to use different approaches using useLyaoutEffect() but nothing made logsRef.current.scrollTop value change, it stayed zero all the time.
//my Logs.jsx component
import { useEffect, useRef } from "react";
import Container from "./UI/Container";
import styles from "./Logs.module.css";
const Logs = ({ logs }) => {
const logsRef = useRef(null);
useEffect(() => {
logsRef.current.scrollTop = logsRef.current.scrollHeight;
console.log(logs);
console.log(logsRef.current.scrollTop);
}, [logs]);
return (
<Container className={`${styles.logs} ${styles.container}`}>
<div ref={logsRef}>
{" "}
{logs.map((log, index) => (
<p key={index}>{log}</p>
))}
</div>
</Container>
);
};
export default Logs;
Also, I do render my Logs.jsx in BattlePhase.jsx component where I do my attack logic on click and I save logs using useState() hook.
//parts where i do save my logs in BattlePhase.jsx
const [logs, setLogs] = useState([]);
const attackHandler = () => {
//logs where pokemon on left attacked pokemon on right
setLogs((prevLogs) => [
...prevLogs,
`${pokemonDataOne.name} attacked ${
pokemonDataTwo.name
} for ${attack.toFixed(2)} dmg`,
`${pokemonDataTwo.name} died`,
])
}
...
<Attack className={isActiveArrow}>
<Button onClick={attackHandler}>Attack!</Button>
</Attack>
Slight long shot but it's possible that the ref is attached to the wrong element. Are you sure the element with the CSS property that makes it scrollable (overflow) isn't on <Container>?
//my Logs.jsx component
import { useLayoutEffect, useRef } from "react";
import Container from "./UI/Container";
import styles from "./Logs.module.css";
const Logs = ({ logs }) => {
const logsRef = useRef(null);
useLayoutEffect(() => {
logsRef.current.scrollTop = logsRef.current.scrollHeight;
console.log(logs);
console.log(logsRef.current.scrollTop);
}, [logs]);
return (
<Container className={`${styles.logs} ${styles.container}`} ref={logsRef}>
<div>
{" "}
{logs.map((log, index) => (
<p key={index}>{log}</p>
))}
</div>
</Container>
);
};
export default Logs;
Also to confirm, you do need useLayoutEffect here.

is it good to settimeout to hold state update?

I had to show fading out of OutterNav when clicked on it and open a model in its place and all this should have transition.
so what I did was to add the className on click of the OutterNav to show the scale and fade transition of OutterNav . But the opening of modal was based on state value which changed on click of OutterNav. So I added a delay in state update using setTimeout.
I had a question that if it is a good way to hold state update in such scenario or there is a good way to do it?
here is some code for reference
import React from "react";
import { useRef } from "react";
const OutterNav = ({ setShowInnerNav }) => {
const containerRef = useRef(null);
const handleClick = () => {
if (containerRef.current) {
containerRef.current.classList.add(
"search-widget-container-animation"
);
}
setTimeout(() => {
setShowInnerNav(true);
}, 300); //is it okay....?
};
return (
<div
className='search-widget-container'
ref={containerRef}
onClick={handleClick}
>
<button className='btn'>
<span className='btn-text'>Anywhere</span>
</button>
<button className='btn'>
<span className='btn-text'>Any week</span>
</button>
</div>
);
};
export default AirbnbOutterNav;

Bug with React: component not rendering until page is refreshed

I have this carousel component that I'm calling from a page but it's not showing until I refresh the page. If I have the Box component and switch to the Carousel component it doesn't work until I refresh the page but if I render the Carousel component first, render Box then go back to Carousel it works. Is this a bug with ReactDOM or what?
My carousel component:
const Carousel = () => {
const { carousel, setCarousel } = useContext(AppContext);
useEffect(() => {
setCarousel(true);
return () => {
setCarousel(false);
};
}, [carousel]);
const items = ["image1.svg", "image2.svg", "image1.svg", "image2.svg"];
const responsive = {
0: { items: 1 },
1024: { items: 2 },
};
return (
<div
className={`container flex`}
style={{
marginTop: "40px",
}}>
<AliceCarousel
disableDotsControls
disableButtonsControls
autoHeight={true}
autoWidth={true}
responsive={responsive}
animationType='slide'>
{items.map((i, index) => (
<img src={i} key={index} alt='' srcSet='' className={Styles.slider} />
))}
</AliceCarousel>
</div>
);
};
And I'm switching/calling it on another component like this:
const { carousel, modalIsOpen, openModal, closeModal } = useContext(
AppContext
);
return (
<div className={carousel ? Styles.layout : ""}>
<div>
<Box />
</div>
</div>
)
I need to make the component re-render when it's called or something so that it works properly, even when I call the AliceCarousel component on my page directly I still have this issue.
Is this something to do with React or the component itself? Thanks
Your useEffect logic leads to infinity loop as after changing the state to true, by having the state in dep array [carousel], you changing it back to false from the cleanup function.
// Useless useEffect, always false.
useEffect(() => {
setCarousel(true);
return () => {
setCarousel(false);
};
}, [carousel]);
See When does the useEffect's callback's return statement execute?

Why do subsequent Alerts in my react app show the "useCountdown hook timer" from the first Alert shown in a given session?

My rubber duck isn't giving me the answer. He is usually smarter.
In my Create-React-App I have an alert context and a react component that handles pop-up alerts throughout the app. I'm playing with React Hooks for the first time and trying to get a countdown timer on each Alert.
I wrote a useCountdown hook (code shown below) which works fine on other test components but when I try to use it with my alerts, every single alert in a given session uses the timer value from the very first alert that fired.
In this screen shot of faulty alerts, each alert shown has very different countdown values, yet they all show the countdown value of the first alert. The countdown is in the upper right-hand corner.
I've played around with useEffect() to try to resolve this but it has become clear I have a bit more to learn regarding hooks and their scoping.
Any suggestions would be much appreciated. Eventually, I will style the timer into a little manual dismiss button with the seconds until auto-dismissal shown as the button's text. For now... just in ugly mode.
useCountdown hook
import { useState, useEffect } from 'react'
const useCountdown = (m = 1, s = 10) => {
const [minutes, setMinutes] = useState(m)
const [seconds, setSeconds] = useState(s)
useEffect(() => {
let myInterval = setInterval(() => {
if (seconds > 0) {
setSeconds(seconds - 1)
}
if (seconds === 0) {
if (minutes === 0) {
clearInterval(myInterval)
} else {
setMinutes(minutes - 1)
setSeconds(59)
}
}
}, 1000)
return () => {
clearInterval(myInterval)
}
})
const finalSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`
return `${minutes}:${finalSeconds}`
}
export default useCountdown
Alert Component
import React, { useContext, Fragment } from 'react'
import AlertContext from '../../context/alert/alertContext'
import { CSSTransition, TransitionGroup } from 'react-transition-group'
import styled from 'styled-components' // TODO Refactor to styled
// Custom hook to give each Alert a countdown timer
import useCountdown from '../../hooks/useCountdown' // BUG!
const Alerts = () => {
// const testAlert = {
// id: 12345,
// title: 'Problem',
// color: 'alert-danger',
// icon: 'fas fa-exclamation-triangle',
// seconds: 20,
// msg: ['This is an alert. Testing 123.', 'Auto-dismiss in 20 seconds.'],
// }
const alertContext = useContext(AlertContext)
const { removeAlert } = alertContext
// ISSUE #32 The initial value gets used through-out session
const countdown = useCountdown(alert.seconds)
return (
<Fragment>
<TransitionGroup>
{alertContext.alerts.length > 0 &&
alertContext.alerts.map((alert) => (
<CSSTransition key={alert.id} timeout={500} classNames='pop'>
<div className={`alert ${alert.color}`}>
<div className='alert-title'>
<h1 onClick={() => removeAlert(alert.id)}>
<i className={`${alert.icon}`} /> <span className='hide-sm'>{alert.title}</span>
</h1>
<span className='alert-countdown'>{countdown}</span>
<button className='btn btn-link btn-sm alert-btn hide-sm' onClick={() => removeAlert(alert.id)}>
<i className='far fa-times-square fa-3x' />
</button>
</div>
<div className='alert-items'>
<ul>
{alert.msg.map((item) => (
<li key={item}>
<i className='fas fa-chevron-circle-right'></i> <span>{item}</span>
</li>
))}
</ul>
</div>
</div>
</CSSTransition>
))}
</TransitionGroup>
</Fragment>
)
}
export default Alerts
The problem is that you're using your useCountdown in the Alerts container component thus storing a single state for all of the child elements. Basically, currently you have a single interval and thus a single countdown value that is reused for all the alerts.
If you want for each of the alerts to have a separate state(countdown values), you should extract the Alert UI into a separate component and use the useCountdown hook there. Then, refactor the Alerts container component to render an Alert component for each instance.
Here is a sample code. It might contain syntax errors because I can't test it right now but try it:
Alerts container component:
import React, { useContext, Fragment } from 'react'
import AlertContext from '../../context/alert/alertContext'
import { CSSTransition, TransitionGroup } from 'react-transition-group'
import styled from 'styled-components' // TODO Refactor to styled
const Alerts = () => {
const alertContext = useContext(AlertContext)
const { removeAlert } = alertContext
return (
<Fragment>
<TransitionGroup>
{alertContext.alerts.length > 0 &&
alertContext.alerts.map((alertInstance) => (
<CSSTransition key={alertInstance.id} timeout={500} classNames='pop'>
<Alert alert={alertInstance} removeAlert={removeAlert}/>
</CSSTransition>
))}
</TransitionGroup>
</Fragment>
)
}
export default Alerts
Alert Component:
export function Alert(props) {
const countdown = useCountdown(props.alert.seconds);
return (
<div className={`alert ${props.alert.color}`}>
<div className='alert-title'>
<h1 onClick={() => props.removeAlert(props.alert.id)}>
<i className={`${props.alert.icon}`} /> <span className='hide-sm'>{props.alert.title}</span>
</h1>
<span className='alert-countdown'>{countdown}</span>
<button className='btn btn-link btn-sm alert-btn hide-sm' onClick={() => props.removeAlert(props.alert.id)}>
<i className='far fa-times-square fa-3x' />
</button>
</div>
<div className='alert-items'>
<ul>
{props.alert.msg.map((item) => (
<li key={item}>
<i className='fas fa-chevron-circle-right'></i> <span>{item}</span>
</li>
))}
</ul>
</div>
</div>
);
}
Also, when altering state that depends on the previous value as in setMinutes(minutes - 1) use the updater function instead of simply supplying a value. In this case you ensure that your update will be accurate and not done over stale data thus resulting in wrong state at the end. You can learn more about that here: useState Reference.
So your hook should look something like this:
const useCountdown = (m = 1, s = 10) => {
const [minutes, setMinutes] = useState(m)
const [seconds, setSeconds] = useState(s)
useEffect(() => {
let myInterval = setInterval(() => {
if (seconds > 0) {
setSeconds((seconds) => seconds - 1)
}
if (seconds === 0) {
if (minutes === 0) {
clearInterval(myInterval)
} else {
setMinutes((minutes) => minutes - 1)
setSeconds(59)
}
}
}, 1000)
return () => {
clearInterval(myInterval)
}
})
const finalSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`
return `${minutes}:${finalSeconds}`
}

addEventListener('scroll') to scrollable <div /> using useRef - React

This is one of the first times I am actually using React Hooks properly in a project so bear with me if I am not quite there.
In the component below, my aim is to display the <HelperTooltip> on load and when the scrolling div (not the window) scrolls I want to hide after it scrolls X amount of pixels.
My thought process is to create a useRef object on the scrolling <div/> element, which then I can add an event listens with a callback function which then can toggle the state to hide the <HelperTooltip>
I have created a Codesandbox below to try and demonstrate what I am trying to do. As you can see in the demo the node.addEventListener('click') is working fine, however when I try and call the node.addEventListener('scroll') it is not firing.
I'm not sure if I taking the wrong approach or not, any help will greatly be appreciated. In the codesandbox demo it is the react image that I trying to hide on scroll, not the <HelperTooltip>
CodeSandbox link: https://codesandbox.io/s/zxj322ln24
import React, { useRef, useCallback, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const App = props => {
const [isLogoActive, toggleLogo] = useState(true);
const scrollElementRef = useCallback(node => {
node.addEventListener("click", event => {
console.log("clicked", event);
});
/*
I want to add the scroll event listener
here and the set the state isLogoActive to
false like the event listener above but the 'scroll' event
is firing --- see below on line 21
*/
// node.addEventListener("scroll", event => {
// console.log("scrolled", event);
// toggle log
// });
});
return (
<div className="scrolling-container">
<div ref={scrollElementRef} className="scrolling-element">
<p>top</p>
{isLogoActive && (
<div className="element-to-hide-after-scroll">
<img
style={{ width: "100px", height: "100px" }}
src="https://arcweb.co/wp-content/uploads/2016/10/react-logo-1000-transparent-768x768.png"
/>
</div>
)}
<p>bottom</p>
</div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("app"));
An easier approach for this particular use case might be to use the onScroll prop and use the scrollTop property from the event target to figure out if you should hide the image or not.
Example
const { useState } = React;
const App = props => {
const [isLogoActive, setLogoActive] = useState(true);
const onScroll = e => {
setLogoActive(e.target.scrollTop < 100);
};
return (
<div onScroll={onScroll} style={{ height: 300, overflowY: "scroll" }}>
<p style={{ marginBottom: 200 }}>top</p>
<img
style={{
width: 100,
height: 100,
visibility: isLogoActive ? "visible" : "hidden"
}}
src="https://arcweb.co/wp-content/uploads/2016/10/react-logo-1000-transparent-768x768.png"
/>
<p style={{ marginTop: 200 }}>bottom</p>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Here is the correct way to bind the addEventListener on div using useRef()
import React, { useState, useEffect, useCallback, useRef } from 'react';
function ScrollingWrapper(props) {
const [hasScrolledDiv, setScrolled] = useState(false);
const scrollContainer = useRef(null);
const onScroll = useCallback((event) => {
if(event.target.scrollTop > 125){
setScrolled(true);
} else if(event.target.scrollTop < 125) {
setScrolled(false);
}
}, []);
useEffect(() => {
scrollContainerWrapper.current.addEventListener('scroll', onScroll);
return () => scrollContainerWrapper.current.removeEventListener('scroll', onScroll);
},[]);
return (
<div ref={scrollContainerWrapper}>
{props.children}
</div>
);
}
export default ScrollingWrapper;
Depending on your use case, it's usually also good to throttle scroll event listeners, so they don't run on every pixel change.
const App = props => {
const [isLogoActive, setLogoActive] = useState(true);
const onScroll = useMemo(() => {
const throttled = throttle(e => setLogoActive(e.target.scrollTop < 100), 300);
return e => {
e.persist();
return throttled(e);
};
}, []);
return (
<div onScroll={onScroll}>
<img
style={{ visibility: isLogoActive ? 'visible' : 'hidden' }}
src="https://arcweb.co/wp-content/uploads/2016/10/react-logo-1000-transparent-768x768.png"
/>
</div>
);
};
The throttle function is available in lodash.
In your example, the scroll is not triggered on the scrolling-element but on the scrolling-container so that's where you want to put your ref : https://codesandbox.io/s/ko4vm93moo :)
But as Throlle said, you could also use the onScroll prop !

Categories