I am writing a react component that can perform a timed session. I am getting a "React useCallback memory leak unmounted component" - I need to mount/unmount the functional component properly - but also on unmounting - as if the user closes the page/tab -- to send a call of the latest timer data before closing the page/browser/tab or navigating away from the page.
I also want to fix this error report. It would be used in a credits system - where people are charged by the timer-- so it needs to reflect the timed session accurately if and when the timer is stopped/closed page.
Also want to join the timer -- with its users -- so if two users joined the session page -- if one starts the timer their end - it starts the timer on the others page -- vice versa if one pauses/stops the timer -- it does the same on the other - so they are both locked in synch - and when the session is stopped - or/and the page is closed - it sends back the latest time without malfunction
research
React Functional component unmount
React useCallback memory leak unmounted component
https://dmitripavlutin.com/react-usecallback/
current sandbox
https://codesandbox.io/s/eloquent-feather-4ijl59
<Timer
sessionPauseHandler={function (duration) {
console.log("duration pause seconds", duration);
}}
sessionEndHandler={function (duration) {
console.log("duration end seconds", duration);
//$20 for 15 mins
let rate = 20;
let sessionQtrSeconds = 15 * 60;
let cost = rate * (duration / sessionQtrSeconds);
console.log("cost", cost);
//https://stackoverflow.com/questions/6162188/javascript-browsers-window-close-send-an-ajax-request-or-run-a-script-on-win
}}
/>
component
import React, { useEffect, useRef, useState, useCallback } from "react";
import Button from "@mui/material/Button";
//import './Timer.scss';
const useTimer = (props) => {
const [renderedStreamDuration, setRenderedStreamDuration] = useState(
"00:00:00"
),
streamDuration = useRef(0),
previousTime = useRef(0),
requestAnimationFrameId = useRef(null),
[isStartTimer, setIsStartTimer] = useState(false),
[isStopTimer, setIsStopTimer] = useState(false),
[isPauseTimer, setIsPauseTimer] = useState(false),
[isResumeTimer, setIsResumeTimer] = useState(false),
isStartBtnDisabled = isPauseTimer || isResumeTimer || isStartTimer,
isStopBtnDisabled = !(isPauseTimer || isResumeTimer || isStartTimer),
isPauseBtnDisabled = !(isStartTimer || (!isStartTimer && isResumeTimer)),
isResumeBtnDisabled = !isPauseTimer;
const updateTimer = useCallback(() => {
let now = performance.now();
let dt = now - previousTime.current;
if (dt >= 1000) {
streamDuration.current = streamDuration.current + Math.round(dt / 1000);
const formattedStreamDuration = new Date(streamDuration.current * 1000)
.toISOString()
.substr(11, 8);
setRenderedStreamDuration(formattedStreamDuration);
previousTime.current = now;
}
requestAnimationFrameId.current = requestAnimationFrame(updateTimer);
}, []);
const startTimer = useCallback(() => {
previousTime.current = performance.now();
requestAnimationFrameId.current = requestAnimationFrame(updateTimer);
}, [updateTimer]);
//console.log("props", props);
useEffect(() => {
// Anything in here is fired on component mount.
// componentDidMount
console.log("MOUNT");
if (isStartTimer && !isStopTimer) {
startTimer();
//console.log("START TIMER");
}
if (isStopTimer && !isStartTimer) {
//console.log("STOP TIMER", streamDuration.current);
props.sessionEndHandler(streamDuration.current);
streamDuration.current = 0;
cancelAnimationFrame(requestAnimationFrameId.current);
setRenderedStreamDuration("00:00:00");
}
// componentWillUnmount
return () => {
console.log("UNMOUNT");
// Anything in here is fired on component unmount.
};
}, [isStartTimer, isStopTimer, startTimer]);
const startHandler = () => {
setIsStartTimer(true);
setIsStopTimer(false);
};
const stopHandler = () => {
setIsStopTimer(true);
setIsStartTimer(false);
setIsPauseTimer(false);
setIsResumeTimer(false);
};
const pauseHandler = () => {
setIsPauseTimer(true);
setIsStartTimer(false);
setIsResumeTimer(false);
cancelAnimationFrame(requestAnimationFrameId.current);
props.sessionPauseHandler(streamDuration.current);
};
const resumeHandler = () => {
setIsResumeTimer(true);
setIsPauseTimer(false);
startTimer();
};
return {
renderedStreamDuration,
isStartBtnDisabled,
isStopBtnDisabled,
isPauseBtnDisabled,
isResumeBtnDisabled,
startHandler,
stopHandler,
pauseHandler,
resumeHandler
};
};
export default function Timer(props) {
const {
renderedStreamDuration,
isStartBtnDisabled,
isStopBtnDisabled,
isPauseBtnDisabled,
isResumeBtnDisabled,
startHandler,
stopHandler,
pauseHandler,
resumeHandler
} = useTimer(props);
//console.log("props1", props)
return (
<div className="timer-controller-wrapper">
<div className="timer-display">{renderedStreamDuration}</div>
<div className="buttons-wrapper">
<Button
onClick={startHandler}
disabled={isStartBtnDisabled}
variant="contained"
color="primary"
>
Start
</Button>
<Button
onClick={stopHandler}
disabled={isStopBtnDisabled}
variant="contained"
color="secondary"
>
Stop
</Button>
<Button
onClick={pauseHandler}
disabled={isPauseBtnDisabled}
variant="contained"
color="primary"
>
Pause
</Button>
<Button
onClick={resumeHandler}
disabled={isResumeBtnDisabled}
variant="contained"
color="secondary"
>
Resume
</Button>
</div>
</div>
);
}