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

React Transition Group nodeRef问题:动画失效求解决方案

问题描述

我参考的YouTube教程已有一年历史,其中使用React Transition Group时并未涉及nodeRef参数。我引入useRef并将其绑定到nodeRef后,代码可正常运行,但过渡动画无法生效。希望了解nodeRef的作用,以及如何解决动画不生效的问题。

相关代码如下:

App组件(根组件)

import React, { useState, useMemo } from "react";
import "./Styles/App.css";
import Postlist from "./components/Postlist";
import MyButton from "./components/UI/button/MyButton";
import PostForm from "./components/PostForm";
import PostFilter from "./components/PostFilter";
import MyModal from "./components/UI/MyModale/MyModale";

function App() {
  const [posts, setPosts] = useState([
    { id: 1, title: "hello", body: "education" },
    { id: 2, title: "world ", body: "world" },
    { id: 3, title: "Javascript ", body: "syntax" },
  ]);

  const [filter, setFilter] = useState({ sort: "", query: "" });
  const [modal, setModal] = useState(false);

  const sortedPosts = useMemo(() => {
    if (filter.sort) {
      return [...posts].sort((a, b) =>
        a[filter.sort].localeCompare(b[filter.sort])
      );
    } else {
      return posts;
    }
  }, [filter.sort, posts]);

  const sortedAndSearchedPosts = useMemo(() => {
    return sortedPosts.filter((post) =>
      post.title.toLowerCase().includes(filter.query.toLowerCase())
    );
  }, [filter.query, sortedPosts]);

  const createForm = (newPost) => {
    setPosts([...posts, newPost]);
    setModal(false);
  };
  const removePost = (post) => {
    setPosts(posts.filter((p) => p.id !== post.id));
  };

  return (
    <div className="App">
      <MyButton style={{ marginTop: 30 }} onClick={() => setModal(true)}>
        Create Customer
      </MyButton>

      <MyModal visible={modal} setVisible={setModal}>
        <PostForm create={createForm} />
      </MyModal>

      <hr style={{ margin: "15px 0" }} />

      <PostFilter filter={filter} setFilter={setFilter} />

      <Postlist
        remove={removePost}
        posts={sortedAndSearchedPosts}
        title="Posts about JS"
      ></Postlist>
    </div>
  );
}

export default App;

Postlist组件

import React from "react";
import Postitem from "./Postitem";
import { TransitionGroup, CSSTransition } from "react-transition-group";
import { useRef } from "react";

const Postlist = ({ posts, title, remove }) => {
  const ref = useRef();

  if (!posts.length) {
    return <h1 style={{ textAlign: "center" }}>Posts can't be found</h1>;
  }

  return (
    <div>
      <h1 style={{ textAlign: "center" }}>{title}</h1>
      <TransitionGroup>
        {posts.map((post, index) => (
          <CSSTransition
            key={post.id}
            nodeRef={ref}
            timeout={500}
            classNames="post"
          >
            <Postitem remove={remove} number={index + 1} post={post} />
          </CSSTransition>
        ))}
      </TransitionGroup>
    </div>
  );
};

export default Postlist;

App.css样式

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

#root {
  display: flex;
  justify-content: center;
}

.App {
  width: 800px;
}

.post {
  display: flex;
  padding: 15px;
  border: 2px solid teal;
  margin-top: 15px;
  justify-content: space-between;
  align-items: center;
}

.post-enter {
  opacity: 0;
}
.post-enter-active {
  transform: translateX(350px);
  transition: all 500ms ease-in;
}
.post-exit {
  opacity: 1;
}
.post-exit-active {
  transform: translateX(-350px);
  transition: all 500ms ease-in;
}

HTML入口文件

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

问题解答

一、nodeRef的作用

在React Transition Group v4+版本中,当CSSTransition包裹的是函数组件,或者项目开启了React的Strict Mode时,React的并发渲染机制会导致过渡组件无法直接获取目标DOM节点的引用。nodeRef的核心作用就是手动传递一个DOM节点的Ref,让过渡组件能够精准定位到要执行动画的元素,确保过渡效果正常触发。

如果没有正确配置nodeRef,在新React版本或Strict Mode环境下,过渡动画会因找不到目标DOM节点而失效。

二、动画不生效的修复方案

你的代码核心问题是:Postlist中只创建了一个共享的ref,所有CSSTransition都复用这个ref,导致每个过渡组件都指向同一个(最后一个)DOM节点,动画无法正确绑定到每个Postitem上。

修复步骤:

  1. 修改Postitem组件,暴露DOM节点Ref
    使用forwardRef让父组件可以将ref传递到Postitem的根DOM节点:

    // Postitem.jsx
    import React, { forwardRef } from 'react';
    
    const Postitem = forwardRef(({ remove, number, post }, ref) => {
      return (
        <div ref={ref} className="post">
          <div>
            <strong>{number}. {post.title}</strong>
            <div>{post.body}</div>
          </div>
          <button onClick={() => remove(post)}>删除</button>
        </div>
      );
    });
    
    export default Postitem;
    
  2. 修改Postlist组件,为每个CSSTransition创建独立Ref
    不再使用单个共享ref,而是为每个post项创建独立的ref,绑定到对应的CSSTransition和Postitem:

    // Postlist.jsx
    import React, { useRef, createRef } from "react";
    import Postitem from "./Postitem";
    import { TransitionGroup, CSSTransition } from "react-transition-group";
    
    const Postlist = ({ posts, title, remove }) => {
      const refs = useRef({});
    
      if (!posts.length) {
        return <h1 style={{ textAlign: "center" }}>Posts can't be found</h1>;
      }
    
      return (
        <div>
          <h1 style={{ textAlign: "center" }}>{title}</h1>
          <TransitionGroup>
            {posts.map((post) => {
              // 为每个post初始化独立ref
              if (!refs.current[post.id]) {
                refs.current[post.id] = createRef();
              }
              return (
                <CSSTransition
                  key={post.id}
                  nodeRef={refs.current[post.id]}
                  timeout={500}
                  classNames="post"
                >
                  <Postitem 
                    ref={refs.current[post.id]}
                    remove={remove} 
                    number={posts.findIndex(p => p.id === post.id) + 1} 
                    post={post} 
                  />
                </CSSTransition>
              );
            })}
          </TransitionGroup>
        </div>
      );
    };
    
    export default Postlist;
    
  3. 优化CSS动画初始状态(可选)
    当前post-enter仅设置了透明度,补充初始位置让动画更连贯:

    .post-enter {
      opacity: 0;
      transform: translateX(-350px);
    }
    .post-enter-active {
      opacity: 1;
      transform: translateX(0);
      transition: all 500ms ease-in;
    }
    

修复原理

每个CSSTransition现在都绑定了对应Postitem的独立DOM节点ref,过渡组件能够准确找到目标元素,正确触发进入/退出的CSS类名切换,动画即可正常生效。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 02:52:05