Reputation: 281
I am writing an app in Next.js 13 and I use framer-motion for animations. It consists of a single page, split in several screens and navigation happens by scrolling to their section ids. I use the old pages
directory.
The animation on one of the screens makes the elements slide in from the side when they are scrolled in view. There is a sidebar on bigger screens, but it's hidden on mobile. Because of this, on bigger screens I have to set a different initial X position of the elements, offset to the right, because otherwise they are hidden underneath the sidebar and framer's whenInView
does not trigger.
I approached this with these pieces of code:
matchMedia
function to provide information for the screen outside of the css files:export const isMobile = (): boolean => {
if (typeof window === 'undefined') return false;
const query = '(min-width: 320px) and (max-width: 767.98px)';
const match = window.matchMedia(query);
return match.matches;
}
<motion.h1
initial={{ x: isMobile() ? '93vw' : '86vw' }}
whileInView={{ x: 0 }}
transition={{ duration: 0.75, type: 'spring' }}
viewport={{ once: true }}
className={classes.element}
>
Example content
</motion.h1>
'use client';
The problem is that I get a hydration error each time I refresh.
Warning: Prop `style` did not match. Server: "transform:translateX(86vw) translateZ(0)" Client: "transform:translateX(93vw) translateZ(0)"
Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
The issue seems fairly obvious, but I am not sure how to solve it. I thought that 'use client'
was supposed to leave this component out of server rendering. Or does it only work only in the new app
directory? Another thing, do you think setting the initial value in an useEffect
hook could solve the issue?
Upvotes: 0
Views: 2715
Reputation: 281
So, I managed to solve it myself. Apparently, framer-motion is smart enough to distinguish client and server when adressing props like style, but in another part of the app I had an entire conditional rendering based on the isMobile()
function. It would resolve to false on the server, because window
is undefined, but would be true on the client when using Chrome dev-tools in responsive mode.
Therefore, this piece of code would break the entire thing, despite the error being about something else:
const renderLeftSide = (position: GridPosition): JSX.Element | null => {
if (isMobile()) return null;
return position === GridPosition.Left ? (
<TimelineCard position={position} />
) : (
<TimelineDate position={position} />
);
};
I solved it by creating a client-only wrapper component:
const ClientOnly = ({ children }: PropsWithChildren) => {
const [clientReady, setClientReady] = useState<boolean>(false);
useEffect(() => {
setClientReady(true);
}, []);
return clientReady ? <>{children}</> : null;
};
Hope this is helpful to someone!
Upvotes: 4