ReactJS开发仿OS窗口:关闭窗口后另一窗口位置重置并重播动画
React仿OS应用窗口状态异常问题修复
问题描述
开发React仿操作系统应用时出现异常:打开窗口1后再打开窗口2,关闭窗口1时,窗口2的位置会重置为默认值,同时重新播放打开动画。
核心代码
App.jsx
import { useState } from "react"; import Header from "./Components/header/Header"; import AppButtons from "./Components/ui/AppButtons"; import WindowsContainer from "./Components/ui/WindowsContainer"; import { WindowsContext } from "./Contexts/WindowsContext"; function App() { const [openedWindows, setOpenedWindows] = useState([]); console.log(openedWindows); return ( <div className="App"> <WindowsContext.Provider value={{ openedWindows, setOpenedWindows }}> <Header /> <AppButtons /> <WindowsContainer /> </WindowsContext.Provider> </div> ); } export default App;
WindowsContainer.jsx
import { AnimatePresence } from "framer-motion"; import React, { useContext } from "react"; import { WindowsContext } from "../../Contexts/WindowsContext"; export default function WindowsContainer() { const { openedWindows } = useContext(WindowsContext); return ( <AnimatePresence> {openedWindows.map((OpenedWindow, index) => { return <OpenedWindow key={index} />; })} </AnimatePresence> ); }
WindowLayout.jsx
import React, { useContext, useState } from "react"; import { Rnd } from "react-rnd"; import { motion } from "framer-motion"; import { WindowsContext } from "../../Contexts/WindowsContext"; export default function WindowLayout({ title, name, size, children }) { const [windowPosition, setWindowPosition] = useState([25, 25]); const { setOpenedWindows } = useContext(WindowsContext); function handleClose() { setOpenedWindows((prev) => { let newArray = [...prev]; let index = newArray.findIndex(function (window) { return window.name === name; }); if (index !== -1) newArray.splice(index, 1); return newArray; }); } function handleDrag(ev) { setWindowPosition([ev.x, ev.y]); } return ( <Rnd default={{ x: windowPosition[0], y: windowPosition[1], width: size[0], height: size[1], }} bounds="window" minWidth={size[0] - 25} minHeight={size[1] - 25} maxWidth={size[0] + 100} maxHeight={size[1] + 100} onDragStop={handleDrag} dragHandleClassName={"handle"} > <motion.div key={name} initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }} transition={{ duration: 0.5 }} className="flex flex-col rounded bg-slate-800 text-white w-full h-full" > <div className="handle bg-slate-900 rounded-t p-2 flex justify-between"> <span>{title}</span> <button onClick={handleClose}> <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} > <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> </svg> </button> </div> {children} </motion.div> </Rnd> ); }
问题根源与修复方案
1. 数组索引作为key的问题
WindowsContainer中使用数组index作为组件key,关闭窗口1后,窗口2的索引从1变为0,React会判定这是新组件并重新挂载,导致位置重置、动画重放。
修复:用窗口唯一标识name作为key(确保每个窗口name不重复):
// WindowsContainer.jsx 修改map逻辑 {openedWindows.map((OpenedWindow) => { return <OpenedWindow key={OpenedWindow.name} />; })}
2. 窗口状态存储位置错误
WindowLayout将windowPosition存在组件内部state,组件重新挂载时会重置为默认值。需把窗口状态提升到全局上下文统一管理。
修复步骤:
- 修改
openedWindows结构,存储包含窗口状态的对象:
// App.jsx 初始化状态改为对象数组 const [openedWindows, setOpenedWindows] = useState([]); // 示例:打开窗口的方法(在触发打开的组件中实现) function openWindow(windowComponent, name, title, size, position = [25,25]) { setOpenedWindows(prev => [...prev, { component: windowComponent, name, title, size, position }]); }
- 更新
WindowsContainer的渲染逻辑,传递窗口状态:
// WindowsContainer.jsx {openedWindows.map((windowItem) => { const WindowComp = windowItem.component; return ( <WindowComp key={windowItem.name} name={windowItem.name} title={windowItem.title} size={windowItem.size} position={windowItem.position} onPositionChange={(newPos) => { setOpenedWindows(prev => prev.map(item => item.name === windowItem.name ? {...item, position: newPos} : item )); }} /> ); })}
- 修改
WindowLayout,移除内部state,接收外部状态和更新方法:
// WindowLayout.jsx export default function WindowLayout({ title, name, size, position, onPositionChange, children }) { const { setOpenedWindows } = useContext(WindowsContext); function handleClose() { setOpenedWindows((prev) => prev.filter(item => item.name !== name)); } function handleDrag(ev) { onPositionChange([ev.x, ev.y]); } return ( <Rnd x={position[0]} y={position[1]} width={size[0]} height={size[1]} bounds="window" minWidth={size[0] - 25} minHeight={size[1] - 25} maxWidth={size[0] + 100} maxHeight={size[1] + 100} onDragStop={handleDrag} dragHandleClassName={"handle"} > <motion.div key={name} initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }} transition={{ duration: 0.5 }} className="flex flex-col rounded bg-slate-800 text-white w-full h-full" > <div className="handle bg-slate-900 rounded-t p-2 flex justify-between"> <span>{title}</span> <button onClick={handleClose}> <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} > <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> </svg> </button> </div> {children} </motion.div> </Rnd> ); }
3. AnimatePresence组件识别问题
确保motion.div的key与组件key一致,让Framer Motion正确识别组件状态,避免不必要的动画触发。
内容的提问来源于stack exchange,提问作者THUNDERDEV
相关产品推荐
相关产品推荐

