DEV Community

Alexandru Călin
Alexandru Călin

Posted on

Simple Route Transitions in Next.js

I'm going to share a straightforward hook-based approach to Next.js page transitions. This article is not focused on CSS but on how to write custom react hooks.
To perform the CSS magic, we're going to use https://mui.com/material-ui/transitions/.

The first step is to identify a way to hijack the page renderer in Next.js, which you do by creating a file called _app.js in the pages folder.

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}
export default MyApp
Enter fullscreen mode Exit fullscreen mode

https://nextjs.org/docs/advanced-features/custom-app
Next.js uses the App component to initialize pages. You can override it and control the page initialization.

Our only interest is that _app.js will run before each page render, thus allowing us to override the layout and enable transition effects.

With this information, we can go ahead and write our custom hook.

import { useEffect, useState } from 'react';

export default function useSimpleRouteTransition({ delay, children }) {
  const [transitionEnd, setTransitionEnd] = useState(true);
  const [displayChildren, setDisplayChildren] = useState(children);

  useEffect(() => {
    setTransitionEnd(false);
    const t = setTimeout(() => {
      setDisplayChildren(children);
      setTransitionEnd(true);
    }, delay);

    return () => {
      clearTimeout(t);
    };
  }, [children]);

  return {
    displayChildren,
    transitionEnd,
  };
}

Enter fullscreen mode Exit fullscreen mode

To initialize, it requires two parameters:

  • delay time in milliseconds for each transition.
  • children are the react elements we receive from _app.js.

Let's analyze the code.

const [transitionEnd, setTransitionEnd] = useState(true);
const [displayChildren, setDisplayChildren] = useState(children);
Enter fullscreen mode Exit fullscreen mode

We define an internal state with true as starting value and make a copy of children.

Diving into the useEffect code.

useEffect(() => {
  setTransitionEnd(false);
  const t = setTimeout(() => {
    setDisplayChildren(children);
    setTransitionEnd(true);
  }, delay);

  return () => {
    clearTimeout(t);
  };
}, [children]);
Enter fullscreen mode Exit fullscreen mode

Every time children changes, a setTimeout is queued, which updates the new children after our set delay. To represent this action, we also toggle our internal transitionEnd from false to true.
Finally, the timeout clears whenever the component unmounts.

Putting everything together into a Layout component, it should look like this:

import Link from 'next/link';
import { Box, Container, Stack, Fade } from '@mui/material';
import useSimpleRouteTransition from '@/hooks/useSimpleRouteTransition';

export default function Layout({ children }) {
  const { transitionEnd, displayChildren } = useSimpleRouteTransition({
    delay: 1000,
    children,
  });

  return (
    <Container maxWidth="lg">
      <Box
        sx={{
          flexFlow: 'column nowrap',
        }}
      >
        <Box mt={10} mb={0}>
          <h1>Page transitions with Next.js</h1>
        </Box>
      </Box>
      <Stack direction={'row'} spacing={2}>
        <Link href="/">index</Link>
        <Link href="/blog">blog</Link>
        <Link href="/links">Links</Link>
      </Stack>
      <Box sx={{ bgcolor: 'green', p: 2 }}>
        <Fade in={transitionEnd} timeout={1000}>
          <div>{displayChildren}</div>
        </Fade>
      </Box>
      <Box sx={{ bgcolor: 'darkblue', p: 2 }}>Footer</Box>
    </Container>
  );
}
Enter fullscreen mode Exit fullscreen mode

Let's examine the implementation.

const { transitionEnd, displayChildren } = useSimpleRouteTransition({
    delay: 1000,
    children,
  });
Enter fullscreen mode Exit fullscreen mode

We call our custom hook with delay: 1000 and children we get from our parent component. From there, we receive displayChildren and transitionEnd.

<Fade in={transitionEnd} timeout={1000}>
  <div>{displayChildren}</div>
</Fade>
Enter fullscreen mode Exit fullscreen mode

In our view, displayChildren instead of children are always shown. We wrap this view in a Fade component, which we set using transitionEnd to achieve a controlled fade.

And that's it! Let me know if it works for you.

You can find all the source code on GitHub:
https://github.com/calinalexandru/next-js-router-transitions

Top comments (0)