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上。
修复步骤:
修改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;修改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;优化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

