[fix] react component runaways, error boundaries (#1625)

This PR fixes a few components that were updating too often. It changes
the format of our error boundaries in order to avoid re-rendering them
as changed props.

### Change Type

- [x] `major` — Breaking change
pull/1626/head
Steve Ruiz 2023-06-20 15:06:28 +01:00 zatwierdzone przez GitHub
rodzic 5cb08711c1
commit 83184aaf43
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
11 zmienionych plików z 55 dodań i 67 usunięć

Wyświetl plik

@ -11,7 +11,6 @@ export default function ErrorBoundaryExample() {
shapes={shapes} shapes={shapes}
tools={[]} tools={[]}
components={{ components={{
ErrorFallback: null, // disable app-level error boundaries
ShapeErrorFallback: ({ error }) => <div>Shape error! {String(error)}</div>, // use a custom error fallback for shapes ShapeErrorFallback: ({ error }) => <div>Shape error! {String(error)}</div>, // use a custom error fallback for shapes
}} }}
onMount={(editor) => { onMount={(editor) => {

Wyświetl plik

@ -770,7 +770,7 @@ export class ErrorBoundary extends React_3.Component<React_3.PropsWithRef<React_
error: Error; error: Error;
}; };
// (undocumented) // (undocumented)
render(): React_3.ReactNode; render(): boolean | JSX.Element | null | number | React_3.ReactFragment | string | undefined;
// (undocumented) // (undocumented)
state: TLErrorBoundaryState; state: TLErrorBoundaryState;
} }
@ -1743,7 +1743,7 @@ export function openWindow(url: string, target?: string): void;
// @internal (undocumented) // @internal (undocumented)
export function OptionalErrorBoundary({ children, fallback, ...props }: Omit<TLErrorBoundaryProps, 'fallback'> & { export function OptionalErrorBoundary({ children, fallback, ...props }: Omit<TLErrorBoundaryProps, 'fallback'> & {
fallback: ((error: unknown) => React_3.ReactNode) | null; fallback: TLErrorFallbackComponent;
}): JSX.Element; }): JSX.Element;
// @public (undocumented) // @public (undocumented)
@ -2261,7 +2261,7 @@ export interface TLEditorComponents {
// (undocumented) // (undocumented)
Cursor: null | TLCursorComponent; Cursor: null | TLCursorComponent;
// (undocumented) // (undocumented)
ErrorFallback: null | TLErrorFallbackComponent; ErrorFallback: TLErrorFallbackComponent;
// (undocumented) // (undocumented)
Grid: null | TLGridComponent; Grid: null | TLGridComponent;
// (undocumented) // (undocumented)
@ -2269,9 +2269,9 @@ export interface TLEditorComponents {
// (undocumented) // (undocumented)
Scribble: null | TLScribbleComponent; Scribble: null | TLScribbleComponent;
// (undocumented) // (undocumented)
ShapeErrorFallback: null | TLShapeErrorFallbackComponent; ShapeErrorFallback: TLShapeErrorFallbackComponent;
// (undocumented) // (undocumented)
ShapeIndicatorErrorFallback: null | TLShapeIndicatorErrorFallback; ShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallback;
// (undocumented) // (undocumented)
SnapLine: null | TLSnapLineComponent; SnapLine: null | TLSnapLineComponent;
// (undocumented) // (undocumented)
@ -2306,7 +2306,9 @@ export interface TLErrorBoundaryProps {
// (undocumented) // (undocumented)
children: React_3.ReactNode; children: React_3.ReactNode;
// (undocumented) // (undocumented)
fallback: (error: unknown) => React_3.ReactNode; fallback: (props: {
error: unknown;
}) => any;
// (undocumented) // (undocumented)
onError?: ((error: unknown) => void) | null; onError?: ((error: unknown) => void) | null;
} }

Wyświetl plik

@ -126,7 +126,7 @@ export const TldrawEditor = memo(function TldrawEditor({
return ( return (
<div ref={setContainer} draggable={false} className="tl-container tl-theme__light" tabIndex={0}> <div ref={setContainer} draggable={false} className="tl-container tl-theme__light" tabIndex={0}>
<OptionalErrorBoundary <OptionalErrorBoundary
fallback={ErrorFallback ? (error) => <ErrorFallback error={error} /> : null} fallback={ErrorFallback}
onError={(error) => annotateError(error, { tags: { origin: 'react.tldraw-before-app' } })} onError={(error) => annotateError(error, { tags: { origin: 'react.tldraw-before-app' } })}
> >
{container && ( {container && (
@ -297,7 +297,7 @@ function TldrawEditorWithReadyStore({
// document in the event of an error to reassure them that their work is // document in the event of an error to reassure them that their work is
// not lost. // not lost.
<OptionalErrorBoundary <OptionalErrorBoundary
fallback={ErrorFallback ? (error) => <ErrorFallback error={error} editor={editor} /> : null} fallback={ErrorFallback}
onError={(error) => onError={(error) =>
editor.annotateError(error, { origin: 'react.tldraw', willCrashApp: true }) editor.annotateError(error, { origin: 'react.tldraw', willCrashApp: true })
} }

Wyświetl plik

@ -1,7 +1,11 @@
/** @public */ /** @public */
export type TLShapeErrorFallbackComponent = (props: { error: unknown }) => any | null export type TLShapeErrorFallbackComponent = (props: { error: any }) => any | null
/** @internal */ /** @internal */
export const DefaultShapeErrorFallback: TLShapeErrorFallbackComponent = () => { export const DefaultShapeErrorFallback: TLShapeErrorFallbackComponent = ({
return <div className="tl-shape-error-boundary" /> error,
}: {
error: any
}) => {
return <div className="tl-shape-error-boundary">{error}</div>
} }

Wyświetl plik

@ -1,10 +1,11 @@
import * as React from 'react' import * as React from 'react'
import { TLErrorFallbackComponent } from './DefaultErrorFallback'
/** @public */ /** @public */
export interface TLErrorBoundaryProps { export interface TLErrorBoundaryProps {
children: React.ReactNode children: React.ReactNode
onError?: ((error: unknown) => void) | null onError?: ((error: unknown) => void) | null
fallback: (error: unknown) => React.ReactNode fallback: (props: { error: unknown }) => any
} }
type TLErrorBoundaryState = { error: Error | null } type TLErrorBoundaryState = { error: Error | null }
@ -30,7 +31,8 @@ export class ErrorBoundary extends React.Component<
const { error } = this.state const { error } = this.state
if (error !== null) { if (error !== null) {
return this.props.fallback(error) const { fallback: Fallback } = this.props
return <Fallback error={error} />
} }
return this.props.children return this.props.children
@ -43,7 +45,7 @@ export function OptionalErrorBoundary({
fallback, fallback,
...props ...props
}: Omit<TLErrorBoundaryProps, 'fallback'> & { }: Omit<TLErrorBoundaryProps, 'fallback'> & {
fallback: ((error: unknown) => React.ReactNode) | null fallback: TLErrorFallbackComponent
}) { }) {
if (fallback === null) { if (fallback === null) {
return <>{children}</> return <>{children}</>

Wyświetl plik

@ -94,6 +94,13 @@ export const Shape = track(function Shape({
const shape = editor.getShapeById(id) const shape = editor.getShapeById(id)
const annotateError = React.useCallback(
(error: any) => {
editor.annotateError(error, { origin: 'react.shape', willCrashApp: false })
},
[editor]
)
if (!shape) return null if (!shape) return null
const util = editor.getShapeUtil(shape) const util = editor.getShapeUtil(shape)
@ -108,12 +115,7 @@ export const Shape = track(function Shape({
draggable={false} draggable={false}
> >
{!isCulled && ( {!isCulled && (
<OptionalErrorBoundary <OptionalErrorBoundary fallback={ShapeErrorFallback} onError={annotateError}>
fallback={ShapeErrorFallback ? (error) => <ShapeErrorFallback error={error} /> : null}
onError={(error) =>
editor.annotateError(error, { origin: 'react.shape', willCrashApp: false })
}
>
<InnerShapeBackground shape={shape} util={util} /> <InnerShapeBackground shape={shape} util={util} />
</OptionalErrorBoundary> </OptionalErrorBoundary>
)} )}
@ -133,12 +135,7 @@ export const Shape = track(function Shape({
{isCulled && util.canUnmount(shape) ? ( {isCulled && util.canUnmount(shape) ? (
<CulledShape shape={shape} /> <CulledShape shape={shape} />
) : ( ) : (
<OptionalErrorBoundary <OptionalErrorBoundary fallback={ShapeErrorFallback} onError={annotateError}>
fallback={ShapeErrorFallback ? (error) => <ShapeErrorFallback error={error} /> : null}
onError={(error) =>
editor.annotateError(error, { origin: 'react.shape', willCrashApp: false })
}
>
<InnerShape shape={shape} util={util} /> <InnerShape shape={shape} util={util} />
</OptionalErrorBoundary> </OptionalErrorBoundary>
)} )}

Wyświetl plik

@ -31,11 +31,7 @@ export const InnerIndicator = ({ editor, id }: { editor: Editor; id: TLShapeId }
if (!shape.shape) return null if (!shape.shape) return null
return ( return (
<OptionalErrorBoundary <OptionalErrorBoundary
fallback={ fallback={ShapeIndicatorErrorFallback}
ShapeIndicatorErrorFallback
? (error) => <ShapeIndicatorErrorFallback error={error} />
: null
}
onError={(error) => onError={(error) =>
editor.annotateError(error, { origin: 'react.shapeIndicator', willCrashApp: false }) editor.annotateError(error, { origin: 'react.shapeIndicator', willCrashApp: false })
} }

Wyświetl plik

@ -39,9 +39,9 @@ export interface TLEditorComponents {
CollaboratorScribble: TLScribbleComponent | null CollaboratorScribble: TLScribbleComponent | null
SnapLine: TLSnapLineComponent | null SnapLine: TLSnapLineComponent | null
Handle: TLHandleComponent | null Handle: TLHandleComponent | null
ErrorFallback: TLErrorFallbackComponent | null ErrorFallback: TLErrorFallbackComponent
ShapeErrorFallback: TLShapeErrorFallbackComponent | null ShapeErrorFallback: TLShapeErrorFallbackComponent
ShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallbackComponent | null ShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallbackComponent
Spinner: TLSpinnerComponent | null Spinner: TLSpinnerComponent | null
} }

Wyświetl plik

@ -38,7 +38,7 @@ function checkAllShapes(editor: Editor, shapes: string[]) {
describe('<TldrawEditor />', () => { describe('<TldrawEditor />', () => {
it('Renders without crashing', async () => { it('Renders without crashing', async () => {
render( render(
<TldrawEditor autoFocus components={{ ErrorFallback: null }}> <TldrawEditor autoFocus>
<div data-testid="canvas-1" /> <div data-testid="canvas-1" />
</TldrawEditor> </TldrawEditor>
) )
@ -49,7 +49,6 @@ describe('<TldrawEditor />', () => {
let editor: Editor let editor: Editor
render( render(
<TldrawEditor <TldrawEditor
components={{ ErrorFallback: null }}
onMount={(e) => { onMount={(e) => {
editor = e editor = e
}} }}
@ -66,7 +65,6 @@ describe('<TldrawEditor />', () => {
let editor: Editor let editor: Editor
render( render(
<TldrawEditor <TldrawEditor
components={{ ErrorFallback: null }}
shapes={defaultShapes} shapes={defaultShapes}
onMount={(e) => { onMount={(e) => {
editor = e editor = e
@ -100,7 +98,6 @@ describe('<TldrawEditor />', () => {
const store = createTLStore({ shapes: [] }) const store = createTLStore({ shapes: [] })
render( render(
<TldrawEditor <TldrawEditor
components={{ ErrorFallback: null }}
store={store} store={store}
onMount={(editor) => { onMount={(editor) => {
expect(editor.store).toBe(store) expect(editor.store).toBe(store)
@ -119,9 +116,13 @@ describe('<TldrawEditor />', () => {
render( render(
<TldrawEditor <TldrawEditor
shapes={defaultShapes} shapes={defaultShapes}
components={{ ErrorFallback: null }}
store={createTLStore({ shapes: [] })} store={createTLStore({ shapes: [] })}
autoFocus autoFocus
components={{
ErrorFallback: ({ error }) => {
throw error
},
}}
> >
<div data-testid="canvas-1" /> <div data-testid="canvas-1" />
</TldrawEditor> </TldrawEditor>
@ -133,9 +134,13 @@ describe('<TldrawEditor />', () => {
expect(() => expect(() =>
render( render(
<TldrawEditor <TldrawEditor
components={{ ErrorFallback: null }}
store={createTLStore({ shapes: defaultShapes })} store={createTLStore({ shapes: defaultShapes })}
autoFocus autoFocus
components={{
ErrorFallback: ({ error }) => {
throw error
},
}}
> >
<div data-testid="canvas-1" /> <div data-testid="canvas-1" />
</TldrawEditor> </TldrawEditor>
@ -150,12 +155,7 @@ describe('<TldrawEditor />', () => {
const initialStore = createTLStore({ shapes: [] }) const initialStore = createTLStore({ shapes: [] })
const onMount = jest.fn() const onMount = jest.fn()
const rendered = render( const rendered = render(
<TldrawEditor <TldrawEditor store={initialStore} onMount={onMount} autoFocus>
components={{ ErrorFallback: null }}
store={initialStore}
onMount={onMount}
autoFocus
>
<div data-testid="canvas-1" /> <div data-testid="canvas-1" />
</TldrawEditor> </TldrawEditor>
) )
@ -165,12 +165,7 @@ describe('<TldrawEditor />', () => {
expect(initialEditor.store).toBe(initialStore) expect(initialEditor.store).toBe(initialStore)
// re-render with the same store: // re-render with the same store:
rendered.rerender( rendered.rerender(
<TldrawEditor <TldrawEditor store={initialStore} onMount={onMount} autoFocus>
components={{ ErrorFallback: null }}
store={initialStore}
onMount={onMount}
autoFocus
>
<div data-testid="canvas-2" /> <div data-testid="canvas-2" />
</TldrawEditor> </TldrawEditor>
) )
@ -180,12 +175,7 @@ describe('<TldrawEditor />', () => {
// re-render with a new store: // re-render with a new store:
const newStore = createTLStore({ shapes: [] }) const newStore = createTLStore({ shapes: [] })
rendered.rerender( rendered.rerender(
<TldrawEditor <TldrawEditor store={newStore} onMount={onMount} autoFocus>
components={{ ErrorFallback: null }}
store={newStore}
onMount={onMount}
autoFocus
>
<div data-testid="canvas-3" /> <div data-testid="canvas-3" />
</TldrawEditor> </TldrawEditor>
) )
@ -199,7 +189,6 @@ describe('<TldrawEditor />', () => {
let editor = {} as Editor let editor = {} as Editor
render( render(
<TldrawEditor <TldrawEditor
components={{ ErrorFallback: null }}
shapes={defaultShapes} shapes={defaultShapes}
tools={defaultTools} tools={defaultTools}
autoFocus autoFocus
@ -322,7 +311,6 @@ describe('Custom shapes', () => {
let editor = {} as Editor let editor = {} as Editor
render( render(
<TldrawEditor <TldrawEditor
components={{ ErrorFallback: null }}
shapes={shapes} shapes={shapes}
tools={tools} tools={tools}
autoFocus autoFocus

Wyświetl plik

@ -1,7 +1,7 @@
import { GeoShapeGeoStyle, preventDefault, useEditor } from '@tldraw/editor' import { GeoShapeGeoStyle, preventDefault, useEditor } from '@tldraw/editor'
import { track, useValue } from '@tldraw/state' import { track, useValue } from '@tldraw/state'
import classNames from 'classnames' import classNames from 'classnames'
import React from 'react' import React, { memo } from 'react'
import { useBreakpoint } from '../../hooks/useBreakpoint' import { useBreakpoint } from '../../hooks/useBreakpoint'
import { useReadonly } from '../../hooks/useReadonly' import { useReadonly } from '../../hooks/useReadonly'
import { TLUiToolbarItem, useToolbarSchema } from '../../hooks/useToolbarSchema' import { TLUiToolbarItem, useToolbarSchema } from '../../hooks/useToolbarSchema'
@ -19,7 +19,7 @@ import { kbdStr } from '../primitives/shared'
import { ToggleToolLockedButton } from './ToggleToolLockedButton' import { ToggleToolLockedButton } from './ToggleToolLockedButton'
/** @public */ /** @public */
export const Toolbar = function Toolbar() { export const Toolbar = memo(function Toolbar() {
const editor = useEditor() const editor = useEditor()
const msg = useTranslation() const msg = useTranslation()
const breakpoint = useBreakpoint() const breakpoint = useBreakpoint()
@ -217,7 +217,7 @@ export const Toolbar = function Toolbar() {
</div> </div>
</div> </div>
) )
} })
const OverflowToolsContent = track(function OverflowToolsContent({ const OverflowToolsContent = track(function OverflowToolsContent({
toolbarItems, toolbarItems,

Wyświetl plik

@ -1,6 +1,6 @@
import { Range, Root, Thumb, Track } from '@radix-ui/react-slider' import { Range, Root, Thumb, Track } from '@radix-ui/react-slider'
import { useEditor } from '@tldraw/editor' import { useEditor } from '@tldraw/editor'
import { useCallback } from 'react' import { memo, useCallback } from 'react'
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey' import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
@ -15,7 +15,7 @@ export interface SliderProps {
} }
/** @internal */ /** @internal */
export function Slider(props: SliderProps) { export const Slider = memo(function Slider(props: SliderProps) {
const { title, steps, value, label, onValueChange } = props const { title, steps, value, label, onValueChange } = props
const editor = useEditor() const editor = useEditor()
const msg = useTranslation() const msg = useTranslation()
@ -59,4 +59,4 @@ export function Slider(props: SliderProps) {
</Root> </Root>
</div> </div>
) )
} })