I apologize for the complexity of the code this question is based around, but it seems the issue itself is arising from the complexity. I wasn't able to replicate the issue with a simpler example. Here is the code repository branch with the issue: https://github.com/chingu-voyages/v42-geckos-team-21/commit/3c20cc55e66e7d0f9122d843222980e404d4910f The left hand (before the change) uses useRef() and does not have the issue, but I don't think useRef() respect's its proper usage.
Here is the main problem code:
import { useState, useRef} from 'react';
import "./Alert.css"
import "animate.css"
import { CSSTransition } from "react-transition-group"
import { time } from 'console';
console.log(typeof CSSTransition);
interface IfcProps {
text: React.ReactNode,
exitAfterDuration: number,
setAlertKey: React.Dispatch<React.SetStateAction<number>>,
alertKey: number
}
const classNames = {
appear: 'animate__bounce',
appearActive: 'animate__bounce',
appearDone: 'animate__bounce',
enter: 'animate__bounce',
enterActive: 'animate__bounce',
enterDone: 'animate__bounce',
exit: 'animate__bounce',
exitActive: 'animate__fadeOut',
exitDone: 'animate__fadeOut'
}
function Alert(props: IfcProps) {
const nodeRef = useRef(null);
let [isIn, setIsIn] = useState(false);
let [previousAlertKey, setPreviousAlertKey] = useState(0);
let [timeoutId, setTimeoutId] = useState<number | null>(null);
// console.log('props', {...props});
console.log('prev, pres', previousAlertKey, props.alertKey)
console.log('state', {isIn, previousAlertKey, timeoutId});
// console.log('prev, current:', previousAlertKey, props.alertKey);
if (props.text === '') {
// do not render if props.text === ''
return null;
} else if (previousAlertKey !== props.alertKey) {
setIsIn(true);
setPreviousAlertKey(oldPreviousAlertKey => {
oldPreviousAlertKey++
return oldPreviousAlertKey;
});
if (timeoutId) {
console.log(timeoutId, 'timeout cleared');
clearTimeout(timeoutId);
}
let localTimeoutId = window.setTimeout(() => {
console.log('executing timeout')
setIsIn(false);
}, props.exitAfterDuration);
console.log({localTimeoutId}, previousAlertKey, props.alertKey);
setTimeoutId(localTimeoutId);
}
return (
<CSSTransition nodeRef={nodeRef} in={isIn} appear={true} timeout={1000} classNames={classNames}>
{/* Using key here to trigger rebounce on alertKey change */}
<div ref={nodeRef} id="alert" className="animate__animated animate__bounce" key={props.alertKey}>
{props.text}
</div>
</CSSTransition>
)
}
export default Alert
Code that resolves issue but probably uses useRef() incorrectly:
import { useState, useRef } from 'react';
import "./Alert.css"
import "animate.css"
import { CSSTransition } from "react-transition-group"
import { time } from 'console';
console.log(typeof CSSTransition);
interface IfcProps {
text: React.ReactNode,
exitAfterDuration: number,
setAlertKey: React.Dispatch<React.SetStateAction<number>>,
alertKey: number
}
const classNames = {
appear: 'animate__bounce',
appearActive: 'animate__bounce',
appearDone: 'animate__bounce',
enter: 'animate__bounce',
enterActive: 'animate__bounce',
enterDone: 'animate__bounce',
exit: 'animate__bounce',
exitActive: 'animate__fadeOut',
exitDone: 'animate__fadeOut'
}
function Alert(props: IfcProps) {
const nodeRef = useRef(null);
const timeoutIdRef = useRef<number | null>(null);
let [isIn, setIsIn] = useState(false);
let [previousAlertKey, setPreviousAlertKey] = useState(0);
console.log({props});
console.log('state', {isIn, previousAlertKey, timeoutIdRef});
console.log('prev, current:', previousAlertKey, props.alertKey);
if (props.text === '') {
// do not render if props.text === ''
return null;
} else if (previousAlertKey !== props.alertKey) {
setIsIn(true);
setPreviousAlertKey(oldPreviousAlertKey => {
oldPreviousAlertKey++
return oldPreviousAlertKey;
});
if (timeoutIdRef.current) {
console.log(timeoutIdRef.current, 'timeout cleared');
clearTimeout(timeoutIdRef.current);
}
let localTimeoutId = window.setTimeout(() => setIsIn(false), props.exitAfterDuration);
console.log({localTimeoutId}, previousAlertKey, props.alertKey);
timeoutIdRef.current = localTimeoutId;
}
return (
<CSSTransition nodeRef={nodeRef} in={isIn} appear={true} timeout={1000} classNames={classNames}>
{/* Using key here to trigger rebounce on alertKey change */}
<div ref={nodeRef} id="alert" className="animate__animated animate__bounce" key={props.alertKey}>
{props.text}
</div>
</CSSTransition>
)
}
export default Alert
The issue shows its head when an invalid row is attempted to be submitted to the database and the Alert component appears. If multiple alerts are triggered in this way, they all disappear when the first setTimeout expires because it was never cleared properly. One timeout should be cleared but because React strict mode renders twice and the creation of a timeout is a side effect, the extra timeout never gets cleared. React isn't aware that there are two timeouts running for every submission attempt (check-mark click).
I'm probably handling my alert component incorrectly, with the alertKey for example.
I feel my problem is related to the fact that my setTimeout is triggered inside the Alert component as opposed to inside the Row component's onClick() handler, as I did so in a simpler example and it did not exhibit the issue.
I fear I may not get any replies as this is a pretty ugly and complex case that requires a fair bit of setup to the dev environment. This may well be a case where I just have to cobble together a solution (e.g. with useRef) and learn the proper React way in the future through experience. Tunnel-vision is one of my faults.