I am trying to make a page transition in Next.js. As the transition is slower than the page load, I need to calculate the missing time for the animation to finish. The transition should last at least 2 seconds, 1s for a fade in of the loading screen, 500ms so that the loading screen stays a little, and 500ms for it to exit.
I am actually trying to achieve this using new Date.getTime()
and getting the amount of time passed since the animation started.
I've tried using the following code to check that the code is correct:
function testDateValues() {
var startTime = new Date().getTime();
var animDuration = 1500; // In milliseconds
setTimeout(() => {
console.log(animDuration - (new Date().getTime() - startTime));
}, 1000);
}
This function prints the time missing after 1 second, which is 500ms (is prints a number between 498-502 but it is correct).
However, when I translate this code to Next.js, I get numbers like -19604
, -60418
, and numbers that have got nothing to do with the remaining animation time.
The code is the following (reduced):
import { useRouter } from 'next/router';
import { useState } from 'react';
export default function LoadingScreen() {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [loadingStartTime, setLoadingStartTime] = useState(new Date().getTime());
useEffect(() => {
const handleStart = () => {
console.log("Loading started");
setLoading(true);
setLoadingStartTime(new Date().getTime());
};
const handleComplete = () => {
console.log("Loading completed");
console.log(1500 - (new Date().getTime() - loadingStartTime));
setTimeout(() => {
setLoading(false);
}, 1500 - (new Date().getTime() - loadingStartTime));
};
router.events.on("routeChangeStart", handleStart);
router.events.on("routeChangeComplete", handleComplete);
router.events.on("routeChangeError", handleComplete);
return () => {
router.events.off("routeChangeStart", handleStart);
router.events.off("routeChangeComplete", handleComplete);
router.events.off("routeChangeError", handleComplete);
};
}, []);
return {loading && (<div>Loading</div>)}
}
That loading screen is in the components/layout.jsx
file.
import LoadingScreen from "./LoadingScreen";
export default function Layout({ children }) {
return (
<>
<motion.main>{children}</motion.main>
<LoadingScreen />
</>
)
}
The loading screen does not even show for a while. There is no page transition.
Why does almost the same code return so different values? (and why -19604 and not 500?)
CodePudding user response:
Your problem is that the loadingStartTime
is initialised when the component loads, and that constant value is what the code in the effect will use. The setLoadingStartTime
call has no effect on the closed-over const
value, all it does is cause the component to re-render. This will not cause the effect to run again however.
There are various ways to solve this:
don't use a state for
loadingStartTime
but simply a mutable variable in the effect handler scope:export default function LoadingScreen() { const router = useRouter(); const [loading, setLoading] = useState(false); useEffect(() => { let loadingStartTime = Date.now(); // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ const handleStart = () => { console.log("Loading started"); setLoading(true); loadingStartTime = Date.now(); // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ }; const handleComplete = () => { console.log("Loading completed"); console.log(1500 - (Date.now() - loadingStartTime)); setTimeout(() => { setLoading(false); }, 1500 - (Date.now() - loadingStartTime)); }; router.events.on("routeChangeStart", handleStart); router.events.on("routeChangeComplete", handleComplete); router.events.on("routeChangeError", handleComplete); return () => { router.events.off("routeChangeStart", handleStart); router.events.off("routeChangeComplete", handleComplete); router.events.off("routeChangeError", handleComplete); }; }, []); return {loading && (<div>Loading</div>)} }
don't use a state for
loadingStartTime
but a constant that is initialised when the transition begins:export default function LoadingScreen() { const router = useRouter(); const [loading, setLoading] = useState(false); useEffect(() => { const handleStart = () => { console.log("Loading started"); setLoading(true); const loadingStartTime = Date.now(); // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ const handleComplete = () => { console.log("Loading completed"); console.log(1500 - (Date.now() - loadingStartTime)); setTimeout(() => { setLoading(false); }, 1500 - (Date.now() - loadingStartTime)); }; router.events.once("routeChangeComplete", handleComplete); router.events.once("routeChangeError", handleComplete); }; router.events.on("routeChangeStart", handleStart); return () => { router.events.off("routeChangeStart", handleStart); }; }, []); return {loading && (<div>Loading</div>)} }
Notice this will keep the complete and error handlers even after the component has unmounted, which may or may not be desirable - or not matter at all.
use two separate effects with proper dependencies:
export default function LoadingScreen() { const router = useRouter(); const [loading, setLoading] = useState(false); const [loadingStartTime, setLoadingStartTime] = useState(Date.now()); useEffect(() => { const handleStart = () => { console.log("Loading started"); setLoading(true); setLoadingStartTime(Date.now()); }; router.events.on("routeChangeStart", handleStart); return () => { router.events.off("routeChangeStart", handleStart); }; }, []); useEffect(() => { // ^^^^^^^^^ console.log("New loadingStartTime", Date(loadingStartTime)); const handleComplete = () => { console.log("Loading completed"); console.log(1500 - (Date.now() - loadingStartTime)); setTimeout(() => { setLoading(false); }, 1500 - (Date.now() - loadingStartTime)); }; router.events.on("routeChangeComplete", handleComplete); router.events.on("routeChangeError", handleComplete); return () => { router.events.off("routeChangeComplete", handleComplete); router.events.off("routeChangeError", handleComplete); }; }, [loadingStartTime]); // ^^^^^^^^^^^^^^^^ return {loading && (<div>Loading</div>)} }