motiondivision / motion

A modern animation library for React and JavaScript
https://motion.dev
MIT License
26.1k stars 855 forks source link

[BUG] useScroll fails to keep track of Element scroll when the ref is passed down to a child component. ONLY happens in Production. #2483

Open eeshankeni opened 11 months ago

eeshankeni commented 11 months ago

UseScroll has a Container prop which is used to keep track of the scroll of a custom container instead of the default scrollable body. Doing so requires a ref to be passed which references the scrollable container. However, if the ref is passed down a component and then the ref is further passed along the component tree then useScroll seems to fail.

https://www.framer.com/motion/use-scroll/

https://github.com/eeshankeni/famer-motion-scroll-bug Now in this case useScroll seems to fail when the ref is passed to a component which is inside of another component (2 levels down).

However, I've noticed that this bug also was happening in a client project when the ref was being passed only 1 component down. I have included this case as well (GreenChildComponent.tsx) in the example but that does not seem to be reproducing.

Steps to reproduce:

yarn install

yarn dev to run development mode. everything works as expected here.

yarn build to build static files.

npx serve@latest out to run the static page. Notice how the animation breaks here.

video demo:

https://github.com/framer/motion/assets/19875557/17c409a7-cc95-4bc3-bb79-e208339fb83a

Edit : Setting layoutEffect:false seems to be a workaround. As documented on https://github.com/framer/motion/issues/2452

Couldn't find any documentation on what layoutEffect does. Was able to get it figured out.

simplypixi commented 9 months ago

Same issue here. Setting layoutEffect to false didn't fix the problem. I've added extra logs and scrollYProgress does not change at all during a scroll.

framer: 10.16 next: 13.5.6 (+css modules).

Abdallah-Abdelkhalek commented 8 months ago

const { scrollXProgress } = useScroll({ layoutEffect: false, <-- This line fixed the problem container: projectsContainerRef, });

VictorAugDB commented 8 months ago

const { scrollXProgress } = useScroll({ layoutEffect: false, <-- This line fixed the problem container: projectsContainerRef, });

Yeah, this fixes the problem, but why does this problem only happen in prod? This is because of SSR or SSG? I don't think so because I had the same problem and I was making use of the 'use client' directive in the entire page. Is the page layout rendered differently in dev? Why? This should happen? Why there isn't anything in the framer motion docs warning about it? I searched in the useScroll section in the docs and there isn't anything warning that this can happen, or detailing what this property does maybe I'm missing something but why the f* there isn't a search bar in the docs?

CroAnna commented 7 months ago

const { scrollXProgress } = useScroll({ layoutEffect: false, <-- This line fixed the problem container: projectsContainerRef, });

this needs to be added in child component, if not clean from answer

nianiam commented 5 months ago

const { scrollXProgress } = useScroll({ layoutEffect: false, <-- This line fixed the problem container: projectsContainerRef, });

Yeah, this fixes the problem, but why does this problem only happen in prod? This is because of SSR or SSG? I don't think so because I had the same problem and I was making use of the 'use client' directive in the entire page. Is the page layout rendered differently in dev? Why? This should happen? Why there isn't anything in the framer motion docs warning about it? I searched in the useScroll section in the docs and there isn't anything warning that this can happen, or detailing what this property does maybe I'm missing something but why the f* there isn't a search bar in the docs?

It only happens in prod because strict mode isn't enabled. Strict mode runs the render function twice to ensure your components are "pure" functions (ie come out with the same results twice). In the first render cycle, the container ref is undefined hence why useScroll doesn't work. The second render cycle properly registers useScroll with the element.

From what I understand useLayoutEffect runs its logic before any painting, so during the first render the ref will be undefined (because the DOM doesn't exist). Because prod doesn't have the second ref cycle your useScroll hook is still registered to an undefined container by the time you're coming to use it in something like a useMotionValueEvent. Setting the layoutEffect option to false presumably make the hook use a regular effect and therefore will re-render properly with the ref value.

There should definitiely be something in the docs about this options. However, people should also understand the tools they're using a bit better.