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

React Portal实现Tldraw全屏模式下删除操作状态同步异常问题

Tldraw全屏Portal模式删除操作退出异常的解决方案

问题复现

  • 进入全屏模式(通过ReactDOM.createPortal配合position: fixed实现)
  • 使用橡皮擦工具删除图形,父组件状态已更新(console.log(editor.getCurrentPageShapes())显示空数据结构)
  • 退出全屏模式,编辑器脱离Portal回到原DOM节点,通过状态传递数据
  • 编辑器未显示删除后的变化,仍保留原有图形;但绘制操作在两种模式下均正常,状态保存无异常

代码示例(中文注释)

import React, { useState, useRef, useEffect } from 'react';
import { Tldraw, TldrawEditor } from '@tldraw/tldraw';
import '@tldraw/tldraw/tldraw.css';

const App = () => {
  // 控制全屏状态
  const [isFullscreen, setIsFullscreen] = useState(false);
  // 存储编辑器图形状态
  const [shapes, setShapes] = useState<any>({});
  // 编辑器容器DOM引用
  const editorContainerRef = useRef<HTMLDivElement>(null);
  // Tldraw编辑器实例引用
  const editorRef = useRef<TldrawEditor | null>(null);

  useEffect(() => {
    // 初始化编辑器实例
    if (editorContainerRef.current && !editorRef.current) {
      editorRef.current = new TldrawEditor({
        container: editorContainerRef.current,
      });

      // 监听编辑器变化,同步到父组件状态
      editorRef.current.on('change', () => {
        setShapes(editorRef.current?.getCurrentPageShapes() || {});
      });
    }
  }, []);

  // 切换全屏/退出全屏
  const toggleFullscreen = () => {
    setIsFullscreen(!isFullscreen);
  };

  return (
    <div style={{ padding: '20px' }}>
      <button onClick={toggleFullscreen}>
        {isFullscreen ? '退出全屏' : '进入全屏'}
      </button>
      {/* 非全屏模式下渲染编辑器 */}
      {!isFullscreen ? (
        <div ref={editorContainerRef} style={{ width: '800px', height: '600px', border: '1px solid #ccc' }}>
          <Tldraw editor={editorRef.current} />
        </div>
      ) : null}
      {/* 全屏模式下通过Portal渲染编辑器 */}
      {isFullscreen && ReactDOM.createPortal(
        <div ref={editorContainerRef} style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 9999, background: 'white' }}>
          <Tldraw editor={editorRef.current} />
        </div>,
        document.body
      )}
    </div>
  );
};

export default App;

核心原因

  1. DOM节点切换导致实例关联异常:全屏和非全屏模式下,编辑器容器的DOM节点实际是两个不同元素(虽共用ref,但挂载的DOM节点会切换),Tldraw实例绑定的是初始DOM节点,退出全屏后新挂载的节点未正确同步删除后的状态。
  2. 删除操作的渲染触发逻辑差异:绘制操作会添加新图形,Tldraw内部会主动触发视图更新;但删除操作仅更新状态,未触发新DOM节点下编辑器的重渲染,因为实例未监听新容器的变化。

解决方案

1. 复用编辑器容器,避免DOM节点切换

不要用Portal切换容器,而是通过修改同一个容器的样式实现全屏,从根源解决实例关联问题:

const App = () => {
  // ...其他状态不变

  return (
    <div style={{ padding: '20px' }}>
      <button onClick={toggleFullscreen}>
        {isFullscreen ? '退出全屏' : '进入全屏'}
      </button>
      <div 
        ref={editorContainerRef} 
        style={{
          width: isFullscreen ? '100vw' : '800px',
          height: isFullscreen ? '100vh' : '600px',
          border: isFullscreen ? 'none' : '1px solid #ccc',
          position: isFullscreen ? 'fixed' : 'static',
          top: isFullscreen ? 0 : 'auto',
          left: isFullscreen ? 0 : 'auto',
          zIndex: isFullscreen ? 9999 : 'auto',
          background: 'white'
        }}
      >
        <Tldraw editor={editorRef.current} />
      </div>
    </div>
  );
};

2. 退出全屏时强制同步状态

如果必须使用Portal,退出时主动调用loadPage方法强制加载最新状态:

const toggleFullscreen = () => {
  if (isFullscreen && editorRef.current) {
    // 获取最新状态并同步
    const latestShapes = editorRef.current.getCurrentPageShapes();
    setShapes(latestShapes);
    editorRef.current.loadPage(latestShapes);
  }
  setIsFullscreen(!isFullscreen);
};

3. 监听容器变化,重新关联实例

若DOM节点必须切换,在全屏状态变化时重新绑定编辑器容器并加载状态:

useEffect(() => {
  if (editorContainerRef.current && editorRef.current) {
    // 重新设置编辑器容器
    editorRef.current.setContainer(editorContainerRef.current);
    // 重新加载当前状态
    editorRef.current.loadPage(shapes);
  }
}, [isFullscreen]);

验证方式

  • 进入全屏删除图形后,退出时检查编辑器视图是否与editor.getCurrentPageShapes()返回的状态一致
  • 测试绘制、删除、修改等操作在两种模式下的状态同步情况

内容的提问来源于stack exchange,提问作者Михаил Морозов

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 07:20:55