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,9 +361,28 @@ 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({
// 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,
})
useLayoutEffect(() => { useLayoutEffect(() => {
editorOptionsRef.current = {
autoFocus,
inferDarkMode,
initialState,
cameraOptions,
}
}, [autoFocus, inferDarkMode, initialState, cameraOptions])
useLayoutEffect(
() => {
const { autoFocus, inferDarkMode, initialState, cameraOptions } = editorOptionsRef.current
const editor = new Editor({ const editor = new Editor({
store, store,
shapeUtils, shapeUtils,
@ -372,7 +391,7 @@ function TldrawEditorWithReadyStore({
getContainer: () => container, getContainer: () => container,
user, user,
initialState, initialState,
autoFocus: initialAutoFocus, autoFocus,
inferDarkMode, inferDarkMode,
cameraOptions, cameraOptions,
assetOptions, assetOptions,
@ -385,20 +404,17 @@ function TldrawEditorWithReadyStore({
return () => { return () => {
editor.dispose() editor.dispose()
} }
}, [ },
container, // if any of these change, we need to recreate the editor.
shapeUtils, [assetOptions, bindingUtils, container, options, shapeUtils, store, tools, user]
bindingUtils, )
tools,
store, // keep the editor up to date with the latest camera options
user, useLayoutEffect(() => {
initialState, if (editor && cameraOptions) {
initialAutoFocus, editor.setCameraOptions(cameraOptions)
inferDarkMode, }
cameraOptions, }, [editor, cameraOptions])
assetOptions,
options,
])
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', () => {