Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Framer-motion drag not respecting previously updated props

A simple use-case is to allow a user to either click buttons to paginate in a slider, or drag. Both events call the same paginate function with a param to either go forward or back--simple stuff.

However, the trigger from drag seems to cause bizarre behavior where the slider wants to start the animation from several slides back as if it ignores the updated props. This doesn't happen when using the buttons and both use the same simple paginate call.

Any tips appreciated.

Minimal example:

export default function App() {
  const [position, setPosition] = useState<number>(0);

  const paginate = (direction: Direction) => {
    setPosition((prev) => {
      return direction === Direction.Forward
        ? Math.max(-800, prev - 200)
        : Math.min(0, prev + 200);
    });
  };

  return (
    <div className="App">
      <Slider>
        <Wrapper
          animate={{ x: position }}
          transition={{
            x: { duration: 1, type: "tween" }
          }}
          drag="x"
          dragConstraints={{
            top: 0,
            left: 0,
            right: 0,
            bottom: 0
          }}
          onDragEnd={(e, { offset, velocity }) => {
            const swipe = swipePower(offset.x, velocity.x);

            if (swipe < -swipeConfidenceThreshold) {
              paginate(Direction.Forward);
            } else if (swipe > swipeConfidenceThreshold) {
              paginate(Direction.Back);
            }
          }}
        >
          <Slide>1</Slide>
          <Slide className="alt">2</Slide>
          <Slide>3</Slide>
          <Slide className="alt">4</Slide>
          <Slide>5</Slide>
        </Wrapper>
      </Slider>
      <button onClick={() => paginate(Direction.Back)}>prev</button>
      <button onClick={() => paginate(Direction.Forward)}>next</button>
    </div>
  );
}

Codesandbox Demo

like image 717
Samuel Goldenbaum Avatar asked Jun 09 '21 19:06

Samuel Goldenbaum


1 Answers

I have to say, this problem is quite interesting. However, I think I figured out a way for you to handle this. One thing I noticed is that if you comment out

onDragEnd={(e, { offset, velocity }) => {
            // const swipe = swipePower(offset.x, velocity.x);
            // if (swipe < -swipeConfidenceThreshold) {
            // paginate(Direction.Forward);
            // } else if (swipe > swipeConfidenceThreshold) {
            // paginate(Direction.Back);
            // }
          }}

the entire onDragEnd prop function, this example still doesn't work, since by the looks of things, the draggable component is not respecting your offset.

I realized that at this point, the problem is the internal state of the component is out of sync with your state. And would you look at that, the Framer Motion API actually provides a way to inspect this. https://www.framer.com/api/motion/motionvalue/#usemotionvalue

It's the hook useMotionValue() which allows us to see what's actually happening. Turns out, our value is being set wrong when the user starts dragging:

useEffect(
    () =>
      motionX.onChange((latest) => {
        console.log("LATEST: ", latest);
      }),
    []
  );

We can see this, because the state "jumps" to 200 as soon as we start dragging.

So fixing in theory is easy, we just need to make sure to let that value "know" about our offset, and that way it's gonna start with the proper offset in mind!

Anyway, that was my thought process, here's the solution, all you need to do is set the left constraint to make it work:

dragConstraints={{
            top: 0,
            left: position,
            right: 0,
            bottom: 0
          }}

And tada! This makes it work. Here's my working solution: https://codesandbox.io/s/lingering-waterfall-2tsfi?file=/src/App.tsx

like image 141
dev-cyprium Avatar answered Dec 05 '22 03:12

dev-cyprium