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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 16:36:20