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
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,
};
}
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);
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]);
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>
);
}
Let's examine the implementation.
const { transitionEnd, displayChildren } = useSimpleRouteTransition({
delay: 1000,
children,
});
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>
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)