0

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?)

rafael
  • 51
  • 1
  • 2
  • 6
  • You start with a fixed number (`animDuration = 1500`) and roughly every second subtract a number that gets larger every iteration (by roughly 1000). This appears to be doing exactly what you asked it to do – Phil Oct 18 '22 at 03:24
  • Eg iteration #1 `1500 - (~ 1000) = ~ 500`, iteration #2 `1500 - (~2000) = ~ -500`, etc – Phil Oct 18 '22 at 03:27
  • Sorry, but I cannot understand. Where is the iteration? – rafael Oct 23 '22 at 02:53
  • `setInterval()` runs your function every second. Each time it runs is an [_iteration_](https://www.merriam-webster.com/dictionary/iteration) – Phil Oct 23 '22 at 05:19
  • It is not `setInterval()`, it is `setTimeout()` which runs only once – rafael Oct 24 '22 at 22:55
  • Err your code literally has `setInterval` – Phil Oct 24 '22 at 22:56
  • I've already edited it. My mistake. Although for the context of what I'm trying to do I thought it was pretty clear – rafael Oct 24 '22 at 22:58
  • I've re-opened your question but it still needs clarity. The first snippet of code doesn't appear related to the second one at all. When you say you _"get numbers"_, what does that mean? Where are you observing these numbers? Is it from the second `console.log()` in `handleComplete`? – Phil Oct 24 '22 at 23:09
  • @Phil If this is a duplicate at all, it's about [the useState set method not reflecting a change immediately](https://stackoverflow.com/q/54069253/1048572) – Bergi Oct 24 '22 at 23:38

1 Answers1

2

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>)}
    }
    
Bergi
  • 630,263
  • 148
  • 957
  • 1,375