Reactjs creates infinite loop using setTimeout - javascript

Here is my App.js that contains beside Header and the MainView
an AlertDialog. Alert dialog has two state controlled props:
msg={alertMsg} toggleClass={alertToggleClass}
The task of AlertDialog is to show if the className is "alertDlg" and disappear if it's "alertDlg alertDlgClosed". Testing it via the inspector manually (changing className) shows, that it works fine.
Therefore alertToggleClass is set to "alertDlg alertDlgClosed" when initializing, so that the alert dialog is hided by default.
Within the MainView Component (before render())
sendGlobalAlert("Test Alert Msg") gets called which is simply a callback to the showAlert(msg) method in App.js.
Now here goes the tricky part: calling setAlertToggleClass("alertDlg"); in showAlert(msg)-method shows the custom alert dialog as expected. However trying to disable it by calling setAlertToggleClass("alertDlg alertDlgClosed"); within the setTimeout creates an infinite loop to showAlert(msg)-method.
As far as I can see, there is no recursivity is in setTimeout(...).
I can't explain this behavior and would appreciate any helpful hints.
import './App.css';
import AlertDialog from './components/general/alert-dialog/AlertDialog';
import { Header } from './components/general/Header';
import MainView from './components/main/MainView';
import { useState } from "react";
function App() {
const [alertMsg,setAlertMsg] = useState("");
const [alertToggleClass,setAlertToggleClass] = useState("alertDlg alertDlgClosed");
function showAlert(msg){
console.log("Showing alert dialog");
setAlertMsg(msg); // set message
setAlertToggleClass("alertDlg"); // show alert dialog
setTimeout(function() {
if(alertToggleClass === "alertDlg" ){
setAlertToggleClass("alertDlg alertDlgClosed");
console.log("hide alert");
}
// setAlertToggleClass("alertDlg test");
},3500);
}
return (
<div className="container">
<Header/>
<MainView sendGlobalAlert={showAlert}/>
<AlertDialog msg={alertMsg} toggleClass={alertToggleClass} />
</div>
);
}
export default App;

The sendGlobalAlert("Test Alert Msg"); within the MainView was the issue here.
After this call, which is basically a callback to App.js's showAlert(msg) method, the props stored in state, that is used by AlertDialog, were changed. Due to an update to these props, AlertDialog rerendered and that again caused a rerender in App.js, which means that it elements had to rerender too. As it was including MainView, the rerender of App.js meant a rerender of MainView, which calls the sendGlobalAlert("Test Alert Msg"); and started the recursiveness.
This is how the MainView looks like:
const MainView = ({sendGlobalAlert}) => {
sendGlobalAlert("Test Alert Msg");
return (
<div className="main-view" >
<BillView/>
<InteractionView sendGlobalAlert={sendGlobalAlert}/>
</div>
)
}

Related

Reset isLoading property of NativeBase button when on-press handler fails

I have made a "one shot" button wrapper component which intercepts the onPress event, sets the isLoading property and then calls the original onPress handler. This sets the button to the disabled loading spinner state while a slow API call is made, provides nice UI feedback and possibly prevents the user from double-clicking.
The original onPress handler has form-field validation, and if this fails I want to cancel the process and let the user correct the input data. So I am returning false to indicate this, which my wrapper catches, however I find I cannot set the button's isLoading back to false - it doesn't change the button state nor remove the disabled, so it's stuck spinning forever.
My button wrapper is this:
import React, {useState} from "react"
import {StyleSheet} from "react-native";
import {Button as ButtonNB, IButtonProps, useTheme} from "native-base"
interface IMyButtonProps extends IButtonProps {
oneShot?: boolean
}
const Button = (props: IMyButtonProps) => {
const [isLoading, setIsLoading] = useState(false)
const {colors} = useTheme()
return <ButtonNB px="25"
style={styles.button}
isLoading={isLoading} spinnerPlacement="end" isLoadingText="Saving..." backgroundColor={colors['primary']['500']}
{...props}
onPress={event => {
if (props.oneShot) setIsLoading(true)
if (props.onPress) {
if(!props.onPress(event)) {
console.debug('cancelled loader')
setIsLoading(false) // <--- DOESN'T WORK
}
}
}}
/>
}
export default Button
Calling code simplified:
<Button
onPress={() => onSave()}
oneShot={true}
testID="prepare-submit-button"
>
{saveSubjectButtonText}
</Button>
async function onSave(){
// on validation failure, just stop and return false
if(!validate()){
return false
}
else {
// do api stuff...
// update local state...
navigation.navigate('Home')
}
}
When validation fails, I do get the 'cancelled loader' log, but the setIsLoading(false) has no effect.
I am viewing in iOS, package versions:
"native-base": "~3.4",
"react": "17.0.2",
"react-dom": "~17.0.2",
"react-native": "0.67.4",
Their documentation: https://docs.nativebase.io/button#h3-loading
I've looked at their issues: https://github.com/GeekyAnts/NativeBase/issues?q=is%3Aissue+isLoading+button+is%3Aclosed
I tried an alternative approach which worked, and now I think I have realised the original problem. I think because the setIsLoading(true) in the onPress handler is re-rendering the button due to state change, the remainder of the function closure is the old state scope when the button wasn't spinning anyway.
So this was nothing to do with NativeBase nor ReactNative but a React gotcha which I think has caught me out before. Each time the render function is called, a new scope becomes active, and even though the old scope may still be finishing running code threads in memory they aren't "attached" to the display/DOM any more. At least that's how I picture it.
I changed it to use reference functions to be able to call the "setLoading" from the parent component. Thanks to this answer: Call child function from parent component in React Native
My button component is now defined like this:
import React, {forwardRef, useImperativeHandle, useState} from "react"
const Button = (props: IMyButtonProps, ref) => {
const [isLoading, setIsLoading] = useState(false)
useImperativeHandle(ref, () => ({
cancelLoading: () => { setIsLoading(false) },
}))
...
export default forwardRef(Button)
and called like this from the parent view:
const submitButtonRef = useRef()
async function onSave(){
// on validation failure, tell the button to cancel loading
if(!validate()){
submitButtonRef.current.cancelLoading()
return
}
...
<Button
onPress={() => onSave()}
ref={submitButtonRef}
And now if the validation fails, the spinner is stopped and you can click the button again.

React component rendering multiple times, failing when reloading the page

I have a rails (7.0.2) application and just installed React. I'm very new to react and can't seem to understand why it looks like my component is loading multiple times, the first time with an empty value for props and the second time with the correct values for props.
App.js:
import "./App.css";
import axios from "axios";
import Customers from "./components/customers";
import { useEffect, useState } from "react";
const API_URL = "http://localhost:3000/internal_api/v1/customers";
function getAPIData() {
return axios.get(API_URL).then((response) => response.data);
}
function App() {
const [customers, setCustomers] = useState([]);
useEffect(() => {
let mounted = true;
getAPIData().then((items) => {
if (mounted) {
setCustomers(items);
}
});
return () => (mounted = false);
}, []);
console.log('LOADED App.js');
return (
<div className="App">
<h1>Hello</h1>
<Customers customers={customers} />
</div>
);
}
export default App;
and customers.js:
import React from "react";
function Customers(props) {
console.log('LOADED customers.js');
return (
<div>
<h1>These customers are from the API</h1>
{props.customers.data.map((customer) => {
return (
<div key={customer.id}>
<h2>{customer.id}</h2>
</div>
);
})}
</div>
);
}
export default Customers;
When I remove this part of the code and reload the page, my props come through correctly when looking in console. Then, when I put the code back and save (without reloading), it displays correctly.
{props.customers.data.map((customer) => {
return (
<div key={customer.id}>
<h2>{customer.id}</h2>
</div>
);
However, as soon as I reload again, I get the same following error:
Uncaught TypeError: Cannot read properties of undefined (reading 'map')
It seems as though the first time everything renders, props is empty. Then the second time, it is full with the data. I checked my rails app and it only hits the API once. What am I doing wrong?
More log outputs:
React component rendering multiple times?
React will render fast before completing the request in use Effect
so in first render customers array will be empty
when request is fulfilled, you are changing state, So react will re-render the component
Only component that uses state reloads when the state is changed this is required else UI will not update
failing when reloading the page? | Failed on Initial Load
Since in Initial render customers will have no data customers.data will be undefined so it will not have map
to bypass this error use props.customers?.data && props.customers.data?.map() addding question mark means expression will be evaluated if not undefined
Source - Optional_chaining

Delete images if the user is leaving the component React

I have form with a drag and drop component where I can upload images, after this I send these pictures with axios to my backend and save it on server side and then re-render it in a preview mode. My problem is, that if a user uploads some pictures and after that he switches to another page without submitting the form the added pictures are staying on my server side for no reason.
So the question: I want to check inside my component if a user is leaving, show a prompt and if he clicks on the OK button I want to delete all the added pictures from my backend before leaving. How can I detect this?
My simplified snippet:
function MyComp(props) {
const [Images,setImages] = useState([]) // in this array I store the recieved image's URL
const [isBlocking,setIsBlocking] = useState(true) // used for prompt
const handleSubmit = (event) => {
event.preventDefault();
setIsBlocking(false)
}
return(
<Grid container className={classes.mainGrid} direction="row" spacing={2}>
<Grid item xs={4} xl={4}>
<Prompt when={isBlocking} message="There are unsaved datas. Are you sure you want to leave?"/>
<form className={classes.form} onSubmit={handleSubmit}>
... somecode
</Grid>
</Grid>
)
}
export default MyComp
Thanks in advance
Inside React Function Component you can call the prompt when the user is trying to leave , i.e when the component is unmounting
In Class Component of React you can use React componentWillUnmount() and in Function Component You can use useEffect cleanup function
Code for Function Component
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
export default function Home(props) {
useEffect(() => {
return function cleanup() {
alert("unmounting");
//call api and execute your code here
};
}, []);
return (
<div>
<Link to="/home">
On Click I will unmount this component and go to /home
</Link>
</div>
</Link>
);
}
Code for Class Component
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
export default class Test extends Component {
componentWillUnmount() {
alert('unmounting component');
//call your code here
}
render() {
return (
<div>
<Link to='/home'>
On Click I will unmount this component and go to /home
</Link>
</div>
);
}
}
You can check this codesandbox if you want any ref
When you leave the page, the component method componentWillUnmount() fires. I don't recall how this behaves if you were to simply close the browser window as opposed to just navigating away, nor do I recall how you can escape it and stay on the component, but that should at least be a good starting point for you. Obviously you'd have to do a class extending React.Component for this one instead of a straight function.

React Rendered page is 1 page behind requested page

I have a state in Redux called page and is changed whenever I click a URL to another page. This is to make my website just have one URL. Whenever I request for a new page, it only gets updated on the next click. For example, when I click Login, nothing happens. Then when I click Register, the Login page renders. Then when I click About, the Register page renders. Why is the page rendering 1 page slow? Is there any way to fix this? I want to make a fade animation to the page and then make the new page appear. Is this something to do with having to use nextProps? I've heard about it somewhere but I neither know what it is nor know how to use it.
import React, { Component } from 'react'
// Redux
import { connect } from "react-redux"
// Components
import Login from './routes/Login'
import About from './routes/About'
import Register from './routes/Register'
import Chat from './routes/Chat'
// PropTypes
import PropTypes from 'prop-types'
class Renderer extends Component {
state = {
rendered: <About /> // Default page to render
}
static propTypes = {
page: PropTypes.string.isRequired
}
componentDidUpdate(prevProps) {
const main = document.getElementsByTagName('main')[0] // Where the rendered page should be
if (this.props.page !== prevProps.page) { // If page state changes
main.style.animation = "fadeOut 0.5s forwards" // Fade old page
setTimeout(() => { // After faded
let returned
switch (this.props.page) {
case 'About':
returned = <About />
break
case 'Login':
returned = <Login />
break
case 'Register':
returned = <Register />
break
case 'Chat':
returned = <Chat />
break
default:
returned = <About />
}
this.state.rendered = returned
main.style.animation = "fadeIn 0.5s forwards" // Show new page
}, 500)
}
}
render() {
return this.state.rendered
}
}
const mapStateToProps = state => ({
page: state.page
})
export default connect(mapStateToProps)(Renderer)
Is there a way to render data in the componentDidUpdate() instead of putting it in the state then updating it? Because I think that is the main cause of the problem
I'm guessing that you should call this.setState and not this.state.rendered = returned. Furthermore the animation isn't going to be right this way. I think it works if you call the animation inside the setState callback, which should happen after the component is re-rendered ( see reactjs.org/docs/react-component.html#setstate )

different behavior with componentDidMount than useEffect when using jquery emoji plugin

I’m stuck using a jquery emoji plugin on one of my components until I finish with a custom plugin I’m building.
For some reason, when I call the emoji plugin inside of componentDidMount, everything works except the ability to utilize a custom button to show the emoji modal. When I use a custom button, the emoji plugin doesn’t attach the event to the button.
What’s crazy is that I can use the same exact code in useEffect, and it attaches the event listener to the custom button just fine.
I verified that the event listener is not attached by looking in the web console at events attached to the element after the page loaded.
You can easily reproduce this problem by placing this component somewhere in an app (and importing jquery with the emoji-area plugin):
import React, {useEffect} from 'react';
export default function CommentInput(props) {
useEffect(() => {
const id = props.blurtId,
$wysiwyg = $('#' + id).emojiarea({
button: '#emoji-btn' + id
});
$.emojiarea.path = '/js/jquery/emojis/';
$.emojiarea.icons = {
':smile:' : 'smile.png',
':angry:' : 'angry.png',
':flushed:' : 'flushed.png',
':neckbeard:' : 'neckbeard.png',
':laughing:' : 'laughing.png'
};
}, []);
return (
<>
<textarea id={props.blurtId} className='blurt-comment-input' />
<i id={'emoji-btn' + props.blurtId} className='fa fa-smile emoji-btn' />
</>
)
}
Simply change this to a class component, and you’ll see that within componentDidMount, everything works except the custom button. Any idea what could cause this change in behavior??
Here is the react class component version:
import React, {Component} from 'react';
class CommentInput extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
const id = this.props.blurtId,
$wysiwyg = $('#' + id).emojiarea({
button: '#emoji-btn' + id
});
$.emojiarea.path = '/js/jquery/emojis/';
$.emojiarea.icons = {
':smile:' : 'smile.png',
':angry:' : 'angry.png',
':flushed:' : 'flushed.png',
':neckbeard:' : 'neckbeard.png',
':laughing:' : 'laughing.png'
};
};
render() {
return (
<>
<textarea id={this.props.blurtId} className='blurt-comment-input' />
<i id={'emoji-btn' + this.props.blurtId} className='fa fa-smile emoji-btn' />
</>
)
}
}
export default CommentInput;
There is a difference between when componentDidMount and useEffect fires.
From the useEffect docs :
Unlike componentDidMount and componentDidUpdate, the function passed
to useEffect fires after layout and paint
The big difference between componentDidMount and useEffect is that useEffect is run after every render, not just the first one. Since your render outputs a new element on each render, after the first render the DOM element you attached the emoji thing to doesn't exist anymore, and a new one with the same ID does.
Options:
Use componentDidUpdate to handle the subsequent renders. (You'll still need componentDidMount for the first one.)
Use a callback ref.

Categories

Resources