From cf32a0922199d41f8b428565ee3715755bd85bef Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 8 Jul 2024 11:22:25 +0100 Subject: [PATCH] Fix editor remounting when camera options change (#4089) Currently, the editor gets recreated whenever the camera options (or several other props that are only relevant at initialisation time) get changed. This diff makes it so that: - init-only props are kept in a ref so they don't invalidate the editor (but are used when the editor _does_ get recreated) - camera options are kept up to date in a separate effect ### Change type - [x] `bugfix` ### Release notes Fix an issue where changing `cameraOptions` via react would cause the entire editor to re-render --- packages/editor/src/lib/TldrawEditor.tsx | 88 +++++++++++-------- .../tldraw/src/test/TldrawEditor.test.tsx | 18 ++++ 2 files changed, 70 insertions(+), 36 deletions(-) diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx index aaa6ad7e3..910e1c8af 100644 --- a/packages/editor/src/lib/TldrawEditor.tsx +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -361,44 +361,60 @@ function TldrawEditorWithReadyStore({ setRenderEditor(editor) } - const [initialAutoFocus] = useState(autoFocus) - - useLayoutEffect(() => { - const editor = new Editor({ - store, - shapeUtils, - bindingUtils, - tools, - getContainer: () => container, - user, - initialState, - autoFocus: initialAutoFocus, - inferDarkMode, - cameraOptions, - assetOptions, - options, - }) - - editorRef.current = editor - setRenderEditor(editor) - - return () => { - editor.dispose() - } - }, [ - container, - shapeUtils, - bindingUtils, - tools, - store, - user, - initialState, - initialAutoFocus, + // props in this ref can be changed without causing the editor to be recreated. + const editorOptionsRef = useRef({ + // for these, it's because they're only used when the editor first mounts: + autoFocus, inferDarkMode, + initialState, + + // for these, it's because we keep them up to date in a separate effect: cameraOptions, - assetOptions, - options, - ]) + }) + useLayoutEffect(() => { + editorOptionsRef.current = { + autoFocus, + inferDarkMode, + initialState, + cameraOptions, + } + }, [autoFocus, inferDarkMode, initialState, cameraOptions]) + + useLayoutEffect( + () => { + const { autoFocus, inferDarkMode, initialState, cameraOptions } = editorOptionsRef.current + const editor = new Editor({ + store, + shapeUtils, + bindingUtils, + tools, + getContainer: () => container, + user, + initialState, + autoFocus, + inferDarkMode, + cameraOptions, + assetOptions, + options, + }) + + editorRef.current = editor + setRenderEditor(editor) + + return () => { + editor.dispose() + } + }, + // if any of these change, we need to recreate the editor. + [assetOptions, bindingUtils, container, options, shapeUtils, store, tools, user] + ) + + // keep the editor up to date with the latest camera options + useLayoutEffect(() => { + if (editor && cameraOptions) { + editor.setCameraOptions(cameraOptions) + } + }, [editor, cameraOptions]) const crashingError = useSyncExternalStore( useCallback( diff --git a/packages/tldraw/src/test/TldrawEditor.test.tsx b/packages/tldraw/src/test/TldrawEditor.test.tsx index 9e33d468b..bad4652d2 100644 --- a/packages/tldraw/src/test/TldrawEditor.test.tsx +++ b/packages/tldraw/src/test/TldrawEditor.test.tsx @@ -226,6 +226,24 @@ describe('', () => { // but strict mode will cause onMount to be called twice expect(onMount).toHaveBeenCalledTimes(2) }) + + it('allows updating camera options without re-creating the editor', async () => { + const editors: Editor[] = [] + const onMount = jest.fn((editor: Editor) => { + if (!editors.includes(editor)) editors.push(editor) + }) + + const renderer = await renderTldrawComponent(, { + waitForPatterns: false, + }) + + expect(editors.length).toBe(1) + expect(editors[0].getCameraOptions().isLocked).toBe(false) + + renderer.rerender() + expect(editors.length).toBe(1) + expect(editors[0].getCameraOptions().isLocked).toBe(true) + }) }) describe('Custom shapes', () => {