How to display popups with time delay in react.js? - reactjs

I have three custom popups and I am able to display them one after another with time delay.
But there is one scenario I am not able to resolve.There are 3 popups -
1)Welcome Video Popup 2)Profile Popup 3) Custom Notification Popup
I have a custom notification popup modal and this comes at the last before the two popups.
Now I want if there is no WelcomeVideo Popup and Profile popup and user hasn't allow or blocked notifications, my custom Notification Popup should show.
My Custom popup will only show if state setNotificationPopup is true.
Here is the code --
const handleCloseWelcomeMessagePopup = () => {
const firstTimeCheckIn = localStorage.getItem('firstTimeCheckIn');
if (firstTimeCheckIn !== 'true' && isShowWelcomeMessage) {
setTimeout(() => {
setNotificationPopup(true);
}, 5000);
}
};
const handleCloseProfilePopup = () => {
setTimeout(() => {
setNotificationPopup(true);
}, 5000);
setShowProfilePopup(false);
};
useEffect(() => {
const firstTimeCheckIn = localStorage.getItem('firstTimeCheckIn');
if (isAuthenticated && !isShowWelcomeMessage && isShowProfilePopup === false) {
if (firstTimeCheckIn !== null && firstTimeCheckIn !== true) {
setNotificationPopup(true);
}
}
}, []);
return (
{(welcomeMessage || welcomeMessageVideo) && (
<PopupModal
id="welcome-message"
headerText={
welcomeMessage && (
<p
className="fr-view welcome-header-text"
dangerouslySetInnerHTML={{
__html: welcomeMessage,
}}
/>
)
}
showModal={isShowWelcomeMessage}
onCloseFunc={() => {
handleCloseWelcomeMessagePopup();
}}
>
)}
{isShowProfilePopup && (
<PopupModal id="profile-popup" showModal={isShowProfilePopup} onCloseFunc={() => handleCloseProfilePopup()}>
<div>{<ProfileDetails params={params} isFirstTime hideProfilePopup={handleCloseProfilePopup} />}</div>
</PopupModal>
)}
{window?.Notification?.permission === 'default' && (
<PopupModal
id="otpPopup"
custClassName={'notification-popup'}
showModal={notificationPopup}
headerText={
<>
<div className="d-flex">
<Image
className=
'img-responsive notification-logo',
src={`${IMAGE_URL}${popupImage}`}
/>
<div>
<p className="notification-title">Test !</p>
<p className="notification-body">{`Allow notifications so you don't miss announcement or direct messages!`}</p>
</div>
</div>
</>
}
backdrop="static"
modelFooter={
<div className="notification-footer">
<Button className="btn btn-link" variant="secondary" onClick={closeNotification}>
Close
</Button>
<Button className="btn btn-primary" onClick={askNotificationPermission}>
Allow
</Button>
</div>
}
/>
)}
)
Note - The issue is if there is Profile Popup then my custom Notification Popup shows immediately after profile Popup and also after 5 sec.

Let's first understand how setTimeout works.
Javascript is a single-threaded language but not a browser. The browser is running three threads at a time: Js Engine, UI thread, and the timer thread. This timer thread actually controls the setTimeout.
Now when we are calling setTimeout, a timer thread starts the countdown, but our actual callback function does not come into execution before the other functions in the stack are not finished. So if there are other time-consuming functions being executed when time up, the callback of setTimeout will not finish in time.
Hence in your code also the code you wrote is correct the problem is with the use of setTimeout. Once we correct that our code will be executed as the way we want.
I hope this piece of information helped you.
For further understanding the concept of setTimeout please refer to the following link :-
https://www.javascripttutorial.net/javascript-bom/javascript-settimeout/

Maybe you should refactor you code as below.
there should be an array that contains all pop-ups that should be shown, in order.
Once this array is defined, your component will iterate through the array, showing popups.
Below is example
export const SomeComponent =(showPopup1,showPopup2,showPopup3)=> {
const popupsToShow = []
showPupup1 && popupsToShow.append(1)
showPupup2 && popupsToShow.append(2)
showPupup3 && popupsToShow.append(3)
useEffect(()=>{
for (i,e in popupsToShow){
setTimeout(() => {
showPopup(e)
}, i * 1000);
}
},[])
}

Related

Is there any pitfall of using ref as conditional statement?

Context: I am trying to scroll view to props.toBeExpandItem item which keeps changing depending on click event in parent component. Every time a user clicks on some button in parent component I want to show them this list and scroll in the clicked item to view port. Also I am trying to avoid adding ref to all the list items.
I am using react ref and want to add it conditionally only once in my component. My code goes as below. In all cases the option.id === props.toBeExpandItem would be truthy only once in loop at any given point of time. I want to understand will it add any overhead if I am adding ref=null for rest of the loop elements?
export const MyComponent = (
props,
) => {
const rootRef = useRef(null);
useEffect(() => {
if (props.toBeExpandItem && rootRef.current) {
setTimeout(() => {
rootRef.current?.scrollIntoView({ behavior: 'smooth' });
});
}
}, [props.toBeExpandItem]);
return (
<>
{props.values.map((option) => (
<div
key={option.id}
ref={option.id === props.toBeExpandItem ? rootRef : null}
>
{option.id}
</div>
))}
</>
);
};
Depending upon your recent comment, you can get the target from your click handler event. Will this work according to your ui?
const handleClick = (e) => {
e.target.scrollIntoView()
}
return (
<ul>
<li onClick={handleClick}>Milk</li>
<li onclick={handleClick}>Cheese </li>
</ul>
)

Conditional rendering not working after a timeout delay in ReactJS

I'm building a simple website with ReactJS (using the Gatsby framework) and trying to handle some conditional rendering using states and React hooks. What I'm trying to accomplish is to basically have a welcome message display for 4 seconds and then render different content.
Here's the code for the render state change (Note: there are two different newGame states because one renders if it's the first time the user sees this screen, and the other renders if they're returning to the site through cookies).
useEffect(() => {
if ((welcomeScreen === true && phase === 'newGame') || (welcomeScreen === true && phase === 'newGamePlus')) {
const timer = setTimeout(() => {
setPhase('gameStart');
setWelcomeStatus(false);
}, 4000);
return () => clearTimeout(timer);
}
}, [phase, welcomeScreen]);
And here is my state management chain. This may be very unorthodox/clunky since I'm quite new to this, so please feel free to correct my mistakes or guide me towards a better path.
if (phase === 'index') {
return (
<Layout>
<div className="mainContainer">
<h1>Enter your name~</h1>
<form onSubmit={handleClick}>
<input
className="nameInput"
onChange={handleChange}
type="text"
placeholder="What's your name?"
value={name}
/>
<button type="submit" className="nameButton">Submit</button>
{ showErrorMessage ? <ErrorText /> : null }
</form>
</div>
</Layout>
)
} else if (phase === 'newGamePlus' && welcomeScreen === true) {
return (
<Layout>
<div className="mainContainer">
<h1>Welcome back, {cookies.get('userName')}~</h1>
</div>
</Layout>
) }
else if (phase === 'newGame' && welcomeScreen === true) {
return (
<Layout>
<div className="mainContainer">
<h1>Welcome, {savedName}~</h1>
</div>
</Layout>
)
} else if (phase === 'gameStart' && welcomeScreen === false) {
return (
<Layout>
<div className="mainContainer">
<h1>Time to play!</h1>
</div>
</Layout>
)
};
The intended effect is to simply have the welcome screen display the content in the final 'gameStart' phase. At the moment, there is a 4 second delay, and then the site crashes with a 'no render' error message from React. I'd greatly appreciate some insight into what I'm doing terribly wrong here, I'm sure it's probably multiple things!
Thanks!
When using setTimeout(), state updates are not batched. In your codes, setPhase('gameStart') will trigger a state change, so useEffect will be executed again, and setWelcomeStatus(false) will be skipped. You will always end up with phase === 'gameStart' && welcomeScreen === true.
Possible solution:
use only one setState in useEffect, so you need to change the variable phase and welcomeScreen to an object variable instead of two separate variables.

How to pass a callback to a child without triggering it

I have a React app with modal, that pop-ups with rules of the game when one clicks a button. What I want to do is make it so when I click anywhere outside this pop up window it will close. i have three files. app.js, dialog.js, and outsidealerter.js . In my main app.js when I click a button it sets a state to visible, so my element takes it and renders based upon it. my outsideralerer.js basicly detects if there is a click outside anything wrapped with specific tags. Now the problem comes that i have a method that changes the state of visibility in app.js, so in order for outsderalerter.js to use it, I pass it to it so it can have access to my main state and change it so that when a click is outside the zone the pop up window disappears. Kind of works except it closes it down even if i click within a pop up window, because when i pass the value to outsidealerter it considers the whole body as a no click zone. My question is how can I prevent it from triggering and just pass it a value, or is it possible to change the state value of app.js from outsidealerter.js
App.js
updateState() {
this.setState({ isOpen: false });
}
<div id='rule-button'>
<button onClick={(e)=>this.setState({isOpen : true})} id="modalBtn" class="button">Open Rules</button>
</div>
<OutsideAlerter updateParent={ this.updateState.bind(this)}/>
<Dialog isOpen={this.state.isOpen} onClose={(e)=>this.setState({isOpen : false})}>
</Dialog>
outsidealerter.js
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
//alert('You clicked outside of me!');
{this.props.updateParent()};
}
}
I think it will be simpler to have the modal take the full space of the window height and width and just make it invisible except for the content of what you want to show.
We can wrap the modal with onClick={hideModal} and wrap the inner content with onClick={e => e.stopPropagation()} which will prevent our wrapper for triggering the hideModal handler.
class ModalWrapper extends React.Component {
state = { isModalOpen: true };
toggleModal = () => {
this.setState(({ isModalOpen }) => ({
isModalOpen: !isModalOpen
}));
};
render() {
const { isModalOpen } = this.state;
return (
<div className="App">
<button onClick={this.toggleModal}>Open Modal</button>
{isModalOpen && <Modal hideModal={this.toggleModal} />}
</div>
);
}
}
function Modal({ hideModal }) {
return (
<div onClick={hideModal} className="modal">
<div onClick={e => e.stopPropagation()} className="modal__content">
Modal content
</div>
</div>
);
}
Working example

Component showing and triggers onclick

Hi I have this component named "ConfirmSave". I have certain condition before I show this component
{FormStatus.statusMode === "Success" && (
<ConfirmSave FormStatus={FormStatus} />
)}
Inside this component I have this
const ConfirmSave = () => {
return ( <div className="col">
<a
className="btn confirm-btn yes-sign col"
onClick={console.log("TEST")} ) > test</a></div>
}
My problem is, even the link is not yet click the "onClick" its activating, it saying TEST in my log
Oh i am calling the function not the trigger
() => { this.props.removeTaskFunction(todo)

React - Change Button HTML to loader on click

I have an Alerts component which is responsible for rendering alerts from JSON supplied to it's props:
alert.js (cut down for brevity)
createAlert(alerts) {
return alerts.map(content => (
<Col key={content.ID} s={12}>
<div className="chip">
<Icon className="alert">error_outline</Icon>
<p>{content.Comment}</p>
<span onClick={() => this.props.onRead(content.ID)}>
<Icon className="closeAlert">close</Icon>
</span>
</div>
</Col>
));
}
render() {
let content = {};
if (!this.props.content) {
//Alerts are null so they are still loading.. show loader
content = this.createLoader();
} else if (!this.props.content.success){
//Error getting alerts
content = this.createAlertError(this.props.content.error);
}
else if (this.props.content.alerts.length === 0) {
//Alert array is null, therefor no alerts
content = this.createNoAlerts();
} else {
//Render Alerts
content = this.createAlert(this.props.content.alerts);
}
return <div>{content}</div>;
}
}
In the above snippit, you can see that if
this.props.alerts
is an array with elements, then it will run
createAlert()
which will create an array of React Components (in this case its just React-Materialize component which is just a <div></div>)
the part I am interested in is the span with the onClick event
<span onClick={() => this.props.onRead(content.ID)}>
<Icon className="closeAlert">close</Icon>
</span>
This run an event from the parent component.
The method that is run in the parent is as follows:
alertRead(id) {
this.props.deleteAlert(id);
}
What I would like, is some way to add a spinning loader icon into the button on the click, in jQuery it would be:
$(button).on("click", function(){
this.html("<i class='fa fa-spin fa-spinner'></i>"); //Spinner Icon
});
The question is, how do I edit the HTML of the button that is clicked on click?
No Redux version
I don't see any redux relation in the code so I will assume that you are not using it or not using it in this particular flow.
What you need to do is to add state to the Alert component and do two things in onClick handler:
() => { this.props.onRead(content.ID); this.setState({clicked: true});}
Of course you need to have state initialization with clicked: false. Second thing is to use this state in rendering:
{this.state.clicked && <Loader />}
{!this.state.clicked && <YourButton />}
So when clicked show loader when not clicked show button. Above code examples are only for showing you the right path.
Version assuming of Redux using.
If you are using redux then alert needs to have connection with the store like:
connect((state) => ({ isClicked: getIsButtonClicked(state)}), { dispatchClick })(AlertComponent)
And you need to dispatch click action after click ( it will set the store state responsible for that - clicked on true.
() => { this.props.onRead(content.ID); this.props.dispatchClick();}
Also finnaly you need to use this prop in rendering:
{this.props.isClicked && <Loader />}
{!this.props.isClicked && <YourButton />}
This solution will cover all instances of Alert component. I am not covering here the redux part.

Resources