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

React中自定义AutoAnimate弹性动画失效问题求助

问题:React集成AutoAnimate自定义弹性动画不生效

我在React项目中集成AutoAnimate插件,改写了自定义Hook实现弹性动画,但点击添加元素时,新元素直接静态显示,没有弹性动画效果。以下是相关代码,请求协助排查:

自定义Hook代码

import { useRef, useLayoutEffect } from 'react';
import autoAnimate, { getTransitionSizes } from '@formkit/auto-animate';

interface UseAutoAnimateOptions {
  parentElement: HTMLElement;
}

const useBouncyAutoAnimate = ({ parentElement }: UseAutoAnimateOptions) => {
  const ref = useRef(null);

  useLayoutEffect(() => {
    if (ref.current) {
      autoAnimate(parentElement, (el, action, oldCoords: any, newCoords: any) => {
        let keyframes;
        // ... (rest of the code remains the same)
        if (action === 'add') {
            keyframes = [
              { transform: 'scale(0)', opacity: 0 },
              { transform: 'scale(1.15)', opacity: 1, offset: 0.75 },
              { transform: 'scale(1)', opacity: 1 },
            ];
          }
          if (action === 'remove') {
            keyframes = [
              { transform: 'scale(1)', opacity: 1 },
              { transform: 'scale(1.15)', opacity: 1, offset: 0.33 },
              { transform: 'scale(0.75)', opacity: 0.1, offset: 0.5 },
              { transform: 'scale(0.5)', opacity: 0 },
            ];
          }
          if (action === 'remain') {
            const deltaX = oldCoords.left - newCoords.left;
            const deltaY = oldCoords.top - newCoords.top;
            const [widthFrom, widthTo, heightFrom, heightTo] = getTransitionSizes(
              el,
              oldCoords,
              newCoords
            );

            let start: any = { transform: `translate(${deltaX}px, ${deltaY}px)` };
            let mid: any = {
              transform: `translate(${deltaX * -0.15}px, ${deltaY * -0.15}px)`,
              offset: 0.75,
            };
            let end: any = { transform: `translate(0, 0)` };

            if (widthFrom !== widthTo) {
              start.width = `${widthFrom}px`;
              mid.width = `${widthFrom >= widthTo ? widthTo / 1.05 : widthTo * 1.05}px`;
              end.width = `${widthTo}px`;
            }
            if (heightFrom !== heightTo) {
              start.height = `${heightFrom}px`;
              mid.height = `${heightFrom >= heightTo ? heightTo / 1.05 : heightTo * 1.05}px`;
              end.height = `${heightTo}px`;
            }
            keyframes = [start, mid, end];
          }

          return new KeyframeEffect(el, keyframes as any, {
            duration: 600,
            easing: 'ease-out',
          });
      });
    }
  }, [ref]);

  return ref;
};

export default useBouncyAutoAnimate;

使用代码(MultiList组件)

import React, { useRef, useState } from 'react'
import Word from './Word';
import { useAutoAnimate } from '@formkit/auto-animate/react'
import useBouncyAutoAnimate from './bouncy';

interface Word {
    id: number,
    visible: boolean,
    word: string
}

interface MultiListProps {
    words: Word[],
    actives: Word[],
    setWords: React.Dispatch<React.SetStateAction<Word[]>>,
    setActives: React.Dispatch<React.SetStateAction<Word[]>>
}

function MultiList({ words, setWords, actives, setActives }: MultiListProps) {

    const parentRef = useRef<HTMLElement | null>(null);
    const animateRef = useBouncyAutoAnimate({ parentElement: parentRef.current! });

    const [ready, setReady] = useState(false);

    return (
        <div ref={parentRef as any}>
            {
                actives.map(word => (
                    <button onClick={() => {
                        const has = actives.find(w => w.word === word.word);
                        if (!has) return;
                        setActives(x => x.filter(w => w.id !== word.id))
                        const newWords = words.map(el => {
                            if (el.id === word.id)
                            el.visible = true;

                            return el;
                        })

                        setWords(newWords)
                    }} ref={ word.id === (actives.at(-1)?.id || NaN) ? animateRef as any : undefined}>
                        <Word word={word} />
                    </button>
                ))
            }
        </div>
    )
}

export default MultiList

相关元素代码(SelectList组件)

import React, { useState } from 'react'
import Word from './Word';
import classnames from 'classnames';


interface Word {
    id: number,
    visible: boolean,
    word: string
}

interface SelectListProps {
    words: Word[],
    actives: Word[],
    setWords: React.Dispatch<React.SetStateAction<Word[]>>,
    setActives: React.Dispatch<React.SetStateAction<Word[]>>
}

function SelectList({ words, setWords, actives, setActives }: SelectListProps) {

    const [ready, setReady] = useState(false);

    return (
        <div>
            {
                words.map(word => (
                    <button onClick={() => {
                        const has = actives.find(w => w.word === word.word);
                        if (has) return;
                        setActives(x => ([
                            ...x,
                            word
                        ]))

                        const newWords = words.map(el => {
                            if (el.id === word.id)
                            el.visible = false;

                            return el;
                        })

                        setWords(newWords)
                    }} className={classnames('transition-opacity', {
                        'opacity-50': !word.visible
                    })}>
                        <Word word={word} />
                    </button>
                ))
            }
        </div>
    )
}

export default SelectList

问题排查与修复方案

1. 自定义Hook核心错误修复

  • 依赖项与绑定时机错误:原Hook依赖ref但ref.current变化不会触发effect,且初始化时parentElement传入的是未挂载的null。修改后监听parentElement,并在其存在时才绑定动画,同时添加清理函数避免内存泄漏:
import { useRef, useLayoutEffect } from 'react';
import autoAnimate, { getTransitionSizes } from '@formkit/auto-animate';

interface UseAutoAnimateOptions {
  parentElement: HTMLElement | null;
}

const useBouncyAutoAnimate = ({ parentElement }: UseAutoAnimateOptions) => {
  const ref = useRef(null);

  useLayoutEffect(() => {
    if (!parentElement) return;
    
    const cleanup = autoAnimate(parentElement, (el, action, oldCoords: any, newCoords: any) => {
      let keyframes;
      if (action === 'add') {
          keyframes = [
            { transform: 'scale(0)', opacity: 0 },
            { transform: 'scale(1.15)', opacity: 1, offset: 0.75 },
            { transform: 'scale(1)', opacity: 1 },
          ];
        }
        if (action === 'remove') {
          keyframes = [
            { transform: 'scale(1)', opacity: 1 },
            { transform: 'scale(1.15)', opacity: 1, offset: 0.33 },
            { transform: 'scale(0.75)', opacity: 0.1, offset: 0.5 },
            { transform: 'scale(0.5)', opacity: 0 },
          ];
        }
        if (action === 'remain') {
          const deltaX = oldCoords.left - newCoords.left;
          const deltaY = oldCoords.top - newCoords.top;
          const [widthFrom, widthTo, heightFrom, heightTo] = getTransitionSizes(
            el,
            oldCoords,
            newCoords
          );

          let start: any = { transform: `translate(${deltaX}px, ${deltaY}px)` };
          let mid: any = {
            transform: `translate(${deltaX * -0.15}px, ${deltaY * -0.15}px)`,
            offset: 0.75,
          };
          let end: any = { transform: `translate(0, 0)` };

          if (widthFrom !== widthTo) {
            start.width = `${widthFrom}px`;
            mid.width = `${widthFrom >= widthTo ? widthTo / 1.05 : widthTo * 1.05}px`;
            end.width = `${widthTo}px`;
          }
          if (heightFrom !== heightTo) {
            start.height = `${heightFrom}px`;
            mid.height = `${heightFrom >= heightTo ? heightTo / 1.05 : heightTo * 1.05}px`;
            end.height = `${heightTo}px`;
          }
          keyframes = [start, mid, end];
        }

        return new KeyframeEffect(el, keyframes as any, {
          duration: 600,
          easing: 'ease-out',
        });
    });

    return cleanup;
  }, [parentElement]);

  return ref;
};

export default useBouncyAutoAnimate;

2. 使用组件错误修复

  • 无效parentElement传入:直接将parentRef.current传入Hook,由Hook处理null情况;
  • 错误绑定子元素ref:AutoAnimate只需绑定父元素,自动监听子元素增删,无需给单个子元素绑ref;
  • 添加唯一key:React列表必须加key,确保AutoAnimate正确识别元素变化;
  • 避免直接修改原数组:更新状态时返回新对象,确保React检测到状态变化。

修改后的MultiList组件:

import React, { useRef } from 'react'
import Word from './Word';
import useBouncyAutoAnimate from './bouncy';

interface Word {
    id: number,
    visible: boolean,
    word: string
}

interface MultiListProps {
    words: Word[],
    actives: Word[],
    setWords: React.Dispatch<React.SetStateAction<Word[]>>,
    setActives: React.Dispatch<React.SetStateAction<Word[]>>
}

function MultiList({ words, setWords, actives, setActives }: MultiListProps) {

    const parentRef = useRef<HTMLElement | null>(null);
    useBouncyAutoAnimate({ parentElement: parentRef.current });

    return (
        <div ref={parentRef}>
            {
                actives.map(word => (
                    <button 
                        key={word.id}
                        onClick={() => {
                            setActives(x => x.filter(w => w.id !== word.id))
                            setWords(prevWords => 
                                prevWords.map(el => 
                                    el.id === word.id ? {...el, visible: true} : el
                                )
                            )
                        }}
                    >
                        <Word word={word} />
                    </button>
                ))
            }
        </div>
    )
}

export default MultiList

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 05:33:12