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;
核心原因
- DOM节点切换导致实例关联异常:全屏和非全屏模式下,编辑器容器的DOM节点实际是两个不同元素(虽共用ref,但挂载的DOM节点会切换),Tldraw实例绑定的是初始DOM节点,退出全屏后新挂载的节点未正确同步删除后的状态。
- 删除操作的渲染触发逻辑差异:绘制操作会添加新图形,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,提问作者Михаил Морозов
相关产品推荐
相关产品推荐

