FlushBG
FlushBG

Reputation: 281

Next.js 13: Hydration error when conditionally applying initial framer-motion animation value based on screen size

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:

  1. A utility function that wraps the browser's 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;
}
  1. Different values for the element's initial X position, for example:
<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>
  1. Above all the screens that have dynamic initial values, I added:
'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

Answers (1)

FlushBG
FlushBG

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

Related Questions