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
This commit is contained in:
alex 2024-07-08 11:22:25 +01:00 committed by GitHub
parent a9ca00c04c
commit cf32a09221
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 70 additions and 36 deletions

View file

@ -361,44 +361,60 @@ function TldrawEditorWithReadyStore({
setRenderEditor(editor) setRenderEditor(editor)
} }
const [initialAutoFocus] = useState(autoFocus) // props in this ref can be changed without causing the editor to be recreated.
const editorOptionsRef = useRef({
useLayoutEffect(() => { // for these, it's because they're only used when the editor first mounts:
const editor = new Editor({ autoFocus,
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,
inferDarkMode, inferDarkMode,
initialState,
// for these, it's because we keep them up to date in a separate effect:
cameraOptions, 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( const crashingError = useSyncExternalStore(
useCallback( useCallback(

View file

@ -226,6 +226,24 @@ describe('<TldrawEditor />', () => {
// but strict mode will cause onMount to be called twice // but strict mode will cause onMount to be called twice
expect(onMount).toHaveBeenCalledTimes(2) 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(<TldrawEditor onMount={onMount} />, {
waitForPatterns: false,
})
expect(editors.length).toBe(1)
expect(editors[0].getCameraOptions().isLocked).toBe(false)
renderer.rerender(<TldrawEditor onMount={onMount} cameraOptions={{ isLocked: true }} />)
expect(editors.length).toBe(1)
expect(editors[0].getCameraOptions().isLocked).toBe(true)
})
}) })
describe('Custom shapes', () => { describe('Custom shapes', () => {