You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Framer Motion ReOrder组件排序异常问题求助

问题描述

我正在用Framer Motion实现可拖拽重排的列表,目标是做出流畅的拖拽过渡效果。项目基于Formik表单,ingredientList存储食材的create_index,对应values.ingredients数据。

目前核心问题:列表项拖拽时没有示例那样的流畅过渡,而是突兀切换——把第二个项拖到首位时,第一个项不会平滑下移,两者直接突然交换位置。


父组件代码

const IngredientTab = ({values}: any) => {
  const [ingredientList, setIngredientList] = useState(values.ingredients.map((ingredient) => ingredient.create_index))

  return (
    <div css={IngredientTabStyles}>
      <FieldArray name="ingredients">
        {({ insert, remove, push }) => (
          <div>
            <Reorder.Group axis="y" values={ingredientList} onReorder={setIngredientList}>
              <AnimatePresence>
                {ingredientList.map((ingredientUniqueValue, index) => {
                  return (
                    <Ingredient 
                      key={index}
                      index={index} 
                      uniqueValue={ingredientUniqueValue} 
                      ingredients={values.ingredients}
                      order={`${ingredientList.indexOf(ingredientUniqueValue) + 1}`}
                    />
                  )
                })}
              </AnimatePresence>
            </Reorder.Group>
            <button
              type="button"
              className="add-ingredients"
              onClick={() => {
                  setIngredientList([...ingredientList, values.ingredients.length])
                  push({ name: '', additional_info: '', quantity: '', unit_id: '', create_index: values.ingredients.length})
                }}
            >
              Add Ingredient
            </button>
          </div>
      )}
      </FieldArray>
    </div>
  )
}

export default IngredientTab

const IngredientTabStyles = css`
  .ingredient-fields {
    margin-bottom: 2rem;
    background-color: white;
  }
`

子组件代码

const Ingredient = ({ uniqueValue, ingredients, order, ingredient}: any) => {
  const y = useMotionValue(0);
  const boxShadow = useRaisedShadow(y);

  const ingredientIndex = ingredients.findIndex(ingredient => ingredient.create_index==uniqueValue)

  return (
    <Reorder.Item      
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      key={uniqueValue} 
      value={uniqueValue} 
      style={{boxShadow,y}}
    >
      {ingredient}

      <div className="ingredient-fields" css={IngredientStyles}>
        <div className="order">
          <h6>{order}</h6>
        </div>
        <div className="ingredient-name">
          <Field
            name={`ingredients.${ingredientIndex}.name`}
            type='text'
            placeholder="Ingredient"
          />
          <Field
            name={`ingredients.${ingredientIndex}.additional_info`}
            type='text'
            placeholder="Description"
          />
        </div>
        <Field
          name={`ingredients.${ingredientIndex}.quantity`}
          type='number'
          placeholder="Quantity"
        />
      </div>
    </Reorder.Item>
  )
}

export default Ingredient

const IngredientStyles = css`
  display: flex;
  margin-bottom: 2rem;

  .order {
    display: flex;
    align-items: center;
    background-color: ${theme.components.grey};
    padding: 1rem 2rem;
    margin-right: 2rem;
    border-radius: 0.4rem;
  }

  .ingredient-name {
    display: flex;
  }

  input {
    padding-bottom: 1rem;
    border: none;
    border-bottom: 1px solid ${theme.colors.lightGrey};
    font-family: 'Source Sans Pro', sans-serif;
    font-weight: 300;
  }
`

解决方案

关键问题分析

  1. 列表项key使用索引导致动画丢失:用index作为key,重排时索引变化会触发组件销毁重建,破坏Framer Motion的动画状态。
  2. AnimatePresence干扰默认过渡:Reorder.Group内置重排动画逻辑,外部包裹AnimatePresence会打断原有流畅过渡。
  3. key重复定义:子组件Reorder.Item重复设置key,导致React渲染逻辑冲突。
  4. 序号计算冗余:用indexOf遍历数组获取顺序,效率低且易出错。

修改后的父组件代码

const IngredientTab = ({values}: any) => {
  const [ingredientList, setIngredientList] = useState(values.ingredients.map((ingredient) => ingredient.create_index))

  return (
    <div css={IngredientTabStyles}>
      <FieldArray name="ingredients">
        {({ insert, remove, push }) => (
          <div>
            <Reorder.Group axis="y" values={ingredientList} onReorder={setIngredientList}>
              {ingredientList.map((ingredientUniqueValue, index) => {
                return (
                  <Ingredient 
                    key={ingredientUniqueValue} // 使用唯一值作为key,保证稳定性
                    index={index} 
                    uniqueValue={ingredientUniqueValue} 
                    ingredients={values.ingredients}
                    order={`${index + 1}`} // 直接用循环索引生成序号
                  />
                )
              })}
            </Reorder.Group>
            <button
              type="button"
              className="add-ingredients"
              onClick={() => {
                  const newIndex = values.ingredients.length
                  setIngredientList([...ingredientList, newIndex])
                  push({ name: '', additional_info: '', quantity: '', unit_id: '', create_index: newIndex})
                }}
            >
              Add Ingredient
            </button>
          </div>
      )}
      </FieldArray>
    </div>
  )
}

export default IngredientTab

const IngredientTabStyles = css`
  .ingredient-fields {
    margin-bottom: 2rem;
    background-color: white;
  }
`

修改后的子组件代码

const Ingredient = ({ uniqueValue, ingredients, order}: any) => {
  const y = useMotionValue(0);
  const boxShadow = useRaisedShadow(y);

  const ingredientIndex = ingredients.findIndex(ingredient => ingredient.create_index === uniqueValue)

  return (
    <Reorder.Item      
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      value={uniqueValue} 
      style={{boxShadow, y}}
    >
      <div className="ingredient-fields" css={IngredientStyles}>
        <div className="order">
          <h6>{order}</h6>
        </div>
        <div className="ingredient-name">
          <Field
            name={`ingredients.${ingredientIndex}.name`}
            type='text'
            placeholder="Ingredient"
          />
          <Field
            name={`ingredients.${ingredientIndex}.additional_info`}
            type='text'
            placeholder="Description"
          />
        </div>
        <Field
          name={`ingredients.${ingredientIndex}.quantity`}
          type='number'
          placeholder="Quantity"
        />
      </div>
    </Reorder.Item>
  )
}

export default Ingredient

const IngredientStyles = css`
  display: flex;
  margin-bottom: 2rem;

  .order {
    display: flex;
    align-items: center;
    background-color: ${theme.components.grey};
    padding: 1rem 2rem;
    margin-right: 2rem;
    border-radius: 0.4rem;
  }

  .ingredient-name {
    display: flex;
  }

  input {
    padding-bottom: 1rem;
    border: none;
    border-bottom: 1px solid ${theme.colors.lightGrey};
    font-family: 'Source Sans Pro', sans-serif;
    font-weight: 300;
  }
`

修改说明

  • 替换列表项key为唯一的ingredientUniqueValue,避免组件频繁重建,保留动画状态。
  • 移除AnimatePresence,让Reorder.Group的默认重排动画生效。
  • 删除子组件Reorder.Item上重复的key定义。
  • 简化序号计算逻辑,直接使用循环索引生成当前排序的序号。

内容的提问来源于stack exchange,提问作者csch99

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 09:25:42