framer / motion

Open source, production-ready animation and gesture library for React
https://framer.com/motion
MIT License
23.68k stars 801 forks source link

[BUG] layoutId cannot be changed? #1411

Open desiprisg opened 2 years ago

desiprisg commented 2 years ago

Describe the bug

I have noticed that motion component behave as the layoutId provided to them on the first render remains the same forever, even though its passed value changes.

IMPORTANT: Provide a CodeSandbox reproduction of the bug

If this is something that is not intended, I will try to reproduce it in sandbox

Steps to reproduce

Steps to reproduce the behavior: 1) Initialize a state as undefined(or some string) and on the first render give it a different value. 2) Pass this variable as the layoutId in your motion component. 3) Try to transition to a different component with its layoutId being the second value. 4) The transition does not work.

Expected behavior

layoudId should update when passed a new value.

I'm currently setting the layoutId of an image gallery component(to the url of the image) so it would transition to the page with that same image (the image in the page has the correct layoutId). It only works for the first image of the gallery, while the others "snap" to position. If I were to add the layoutId to each image instead of the gallery itself, I would get weird behaviour with AnimatePresence since the images repeat themselves like in the example in the docs.

I would love some feedback on how layoutId works since I cannot find anything else in the docs and it is always used with a constant string like "underline". Are constant values the only ones that are acceptable?

mattgperry commented 2 years ago

Yeah currently it can't be changed. You could pass the same layoutId to key to try rendering it as a new component but this probably won't yield the results you want. But this is definietly a bug, we should change this at some point

desiprisg commented 2 years ago

Thanks for the quick response. I'm currently just doing something like layoutId = {`${id}_${pageOfGallery}`} on the image component instead of the gallery and save the page number in a context to set it in the components I want to transition to. This way, the AnimatePresence does not fight the layout. It really would be a useful addition though since doing other stuff is getting more complex now since I have to keep track of the page all the time.

samselikoff commented 2 years ago

@mattgperry I'm running into something similar and curious about this sentence:

You could pass the same layoutId to key to try rendering it as a new component but this probably won't yield the results you want.

I'm rendering two months side-by-side in a calendar (say, January and February) and there's a Next button that lets me cycle the calendars (similar to the datepicker on airbnb). I'm using layoutId so the right calendar (January) can animate to its new location on the left. layoutId="jan-2022" by itself doesn't work but if I add key="jan-2022" to the same component it does work.

Is this expected, or is it bad Framer Motion code?

desiprisg commented 2 years ago

While I haven't touched framer-motion in some time and I might be wrong, I think that you don't need layoutId at all for this. I'm pretty sure layout would suffice for your use case since this:

If true, this component will automatically animate to its new position when its layout changes

sounds a lot like what you are describing. (https://www.framer.com/docs/component/###layout)

In my case, I wanted to change the layoutId of the component based on some action, and this is apparently not possible yet, so an option was to change the key as well so there would be a new instance so the layoutId would change, but this would mess up other AnimatePresence stuff if I remember correctly, so I found some hacky solution to make it all work. I'm pretty sure that's what @mattgperry was proposing.

I don't see you mentioning changing the layoutId at all, so I'm a bit confused about how this is all coded up. A codesandbox would be great!

I know the question was not directed at me but glad if I could be of help!

samselikoff commented 2 years ago

I actually ended up changing the implementation to a list of months and using layout like you mentioned! I almost have it working great

<AnimatePresence initial={false}>
  {months.map((firstDayOfMonth, i) => (
    <motion.div
      layout="position"
      key={format(firstDayOfMonth, "MMM-yyyy")}
      className="absolute h-[336px] w-[300px]"
      initial={{ x: i === months.length - 1 ? 600 : -300 }}
      animate={{ x: 300 * i }}
      exit={{ x: i === 0 ? -300 : 600 }}
      transition={TRANSITION}
    >
      <div className="px-6">
        <Month firstDay={firstDayOfMonth} />
      </div>
    </motion.div>
  ))}
</AnimatePresence>

My original implementation used a leftMonth and rightMonth and that's why I reached for layoutId – when February moved from right to left it was getting unmounted in one spot then remounted in another.

<div className="relative flex justify-between mt-8">
  <div className="w-1/2 relative" style={{ height: leftBounds.height }}>
    <AnimatePresence initial={false}>
      <motion.div
        key={format(startingMonth, "MMM-yyyy")}
        layoutId={format(startingMonth, "MMM-yyyy")}
        variants={variants}
        custom={{ side: "left", width: leftBounds.width, direction }}
        initial="enter"
        animate="center"
        exit="exit"
        ref={leftRef}
        className="absolute inset-x-0 px-4"
        transition={TRANSITION}
      >
        <Month firstDay={startingMonth} />
      </motion.div>
    </AnimatePresence>
  </div>
  <div className="w-1/2 relative" style={{ height: rightBounds.height }}>
    <AnimatePresence initial={false}>
      <motion.div
        key={format(endingMonth, "MMM-yyyy")}
        layoutId={format(endingMonth, "MMM-yyyy")}
        variants={variants}
        ref={rightRef}
        custom={{ side: "right", width: rightBounds.width, direction }}
        initial="enter"
        animate="center"
        exit="exit"
        className="absolute inset-x-0 px-4"
        transition={TRANSITION}
      >
        <Month firstDay={endingMonth} />
      </motion.div>
    </AnimatePresence>
  </div>
</div>

I'm happier with the new implementation at the moment, though still working through interruptibility.

qstearns commented 1 year ago

Hello! We are currently running into this issue in our team. Is a solution for this one on the roadmap at all?

mattgperry commented 1 year ago

Not currently, no

jrolfs commented 10 months ago

I've run into this quite a few times and have usually been able to work around it, but I'd love to have more control over layoutId animations. @mattgperry, could you provide any guidance on where to start on this if I wanted to try to help work on it?

Thank you!!