diff --git a/apps/electron/yarn.lock b/apps/electron/yarn.lock
index b30388398b..31fd9e2c02 100644
--- a/apps/electron/yarn.lock
+++ b/apps/electron/yarn.lock
@@ -77,6 +77,7 @@ __metadata:
react-dnd: ^16.0.1
react-dnd-html5-backend: ^16.0.1
react-dom: ^18.2.0
+ react-error-boundary: ^4.0.3
react-is: ^18.2.0
serve: ^14.2.0
storybook: ^7.0.2
@@ -14357,6 +14358,17 @@ __metadata:
languageName: node
linkType: hard
+"react-error-boundary@npm:^4.0.3":
+ version: 4.0.3
+ resolution: "react-error-boundary@npm:4.0.3"
+ dependencies:
+ "@babel/runtime": ^7.12.5
+ peerDependencies:
+ react: ">=16.13.1"
+ checksum: 50813803d3f03eb2ca07c1250a4a001ffe054f5b3f49b15ea5f0a4e1108f549bc7d8def15db51d518516997d34cebc663f4276246301b4a40a72e83a784f5c38
+ languageName: node
+ linkType: hard
+
"react-i18next@npm:^12.2.0":
version: 12.2.0
resolution: "react-i18next@npm:12.2.0"
diff --git a/apps/web/src/components/__debug__/client/Editor.tsx b/apps/web/src/components/__debug__/client/Editor.tsx
index 7486db72d5..23d859ba73 100644
--- a/apps/web/src/components/__debug__/client/Editor.tsx
+++ b/apps/web/src/components/__debug__/client/Editor.tsx
@@ -40,13 +40,7 @@ const Editor: React.FC<{
return <>loading...>;
}
return (
-
+
);
};
diff --git a/apps/web/src/components/page-detail-editor.tsx b/apps/web/src/components/page-detail-editor.tsx
index 83c44678c9..c2ee8c14e5 100644
--- a/apps/web/src/components/page-detail-editor.tsx
+++ b/apps/web/src/components/page-detail-editor.tsx
@@ -71,7 +71,6 @@ export const PageDetailEditor: React.FC = ({
height: 'calc(100% - 52px)',
}}
key={pageId}
- blockSuiteWorkspace={blockSuiteWorkspace}
mode={isPublic ? 'page' : currentMode}
page={page}
onInit={useCallback(
diff --git a/apps/web/src/pages/_debug/broadcast.dev.tsx b/apps/web/src/pages/_debug/broadcast.dev.tsx
index ee29429ba6..c4497ffc7f 100644
--- a/apps/web/src/pages/_debug/broadcast.dev.tsx
+++ b/apps/web/src/pages/_debug/broadcast.dev.tsx
@@ -31,9 +31,6 @@ const BroadcastPage: React.FC = () => {
const [provider, setProvider] = useState(
null
);
- useEffect(() => {
- globalThis.currentBlockSuiteWorkspace = blockSuiteWorkspace;
- }, [blockSuiteWorkspace]);
useEffect(() => {
const provider = createBroadCastChannelProvider(blockSuiteWorkspace);
setProvider(provider);
diff --git a/packages/component/package.json b/packages/component/package.json
index 51fc8a866c..3414e89bbe 100644
--- a/packages/component/package.json
+++ b/packages/component/package.json
@@ -37,6 +37,7 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
+ "react-error-boundary": "^4.0.3",
"react-is": "^18.2.0"
},
"devDependencies": {
diff --git a/packages/component/src/components/block-suite-editor/index.stories.tsx b/packages/component/src/components/block-suite-editor/index.stories.tsx
index cacae2f9b3..e1494f46ce 100644
--- a/packages/component/src/components/block-suite-editor/index.stories.tsx
+++ b/packages/component/src/components/block-suite-editor/index.stories.tsx
@@ -5,6 +5,7 @@ import type { Page } from '@blocksuite/store';
import { Workspace } from '@blocksuite/store';
import { expect } from '@storybook/jest';
import type { Meta, StoryFn } from '@storybook/react';
+import { useState } from 'react';
import type { EditorProps } from '.';
import { BlockSuiteEditor } from '.';
@@ -40,19 +41,26 @@ export default {
component: BlockSuiteEditor,
} satisfies BlockSuiteMeta;
-const Template: StoryFn = (args: EditorProps) => {
+const Template: StoryFn = (props: EditorProps) => {
return (
-
+ <>
+
+
+ >
);
};
+
export const Empty = Template.bind({});
Empty.play = async ({ canvasElement }) => {
const editorContainer = canvasElement.querySelector(
- '[data-testid="editor-test-page0"]'
+ '[data-testid="editor-page0"]'
) as HTMLDivElement;
expect(editorContainer).not.toBeNull();
await new Promise(resolve => {
@@ -67,3 +75,45 @@ Empty.play = async ({ canvasElement }) => {
Empty.args = {
mode: 'page',
};
+
+export const Error: StoryFn = () => {
+ const [props, setProps] = useState>({
+ page: null!,
+ onInit: null!,
+ });
+ return (
+ {
+ setProps({
+ page,
+ onInit: initPage,
+ });
+ }}
+ />
+ );
+};
+
+Error.play = async ({ canvasElement }) => {
+ {
+ const editorContainer = canvasElement.querySelector(
+ '[data-testid="editor-page0"]'
+ );
+ expect(editorContainer).toBeNull();
+ }
+ {
+ const button = canvasElement.querySelector(
+ '[data-testid="error-fallback-reset-button"]'
+ ) as HTMLButtonElement;
+ expect(button).not.toBeNull();
+ button.click();
+ await new Promise(resolve => setTimeout(() => resolve(), 50));
+ }
+ {
+ const editorContainer = canvasElement.querySelector(
+ '[data-testid="editor-page0"]'
+ );
+ expect(editorContainer).not.toBeNull();
+ }
+};
diff --git a/packages/component/src/components/block-suite-editor/index.tsx b/packages/component/src/components/block-suite-editor/index.tsx
index 4d91985ff6..f409bdd6c1 100644
--- a/packages/component/src/components/block-suite-editor/index.tsx
+++ b/packages/component/src/components/block-suite-editor/index.tsx
@@ -1,11 +1,13 @@
import type { BlockHub } from '@blocksuite/blocks';
import { EditorContainer } from '@blocksuite/editor';
-import type { Page, Workspace } from '@blocksuite/store';
-import type { CSSProperties } from 'react';
-import { useEffect, useRef } from 'react';
+import { assertExists } from '@blocksuite/global/utils';
+import type { Page } from '@blocksuite/store';
+import type { CSSProperties, ReactElement } from 'react';
+import { memo, useCallback, useEffect, useRef } from 'react';
+import type { FallbackProps } from 'react-error-boundary';
+import { ErrorBoundary } from 'react-error-boundary';
export type EditorProps = {
- blockSuiteWorkspace: Workspace;
page: Page;
mode: 'page' | 'edgeless';
onInit: (page: Page, editor: Readonly) => void;
@@ -13,51 +15,45 @@ export type EditorProps = {
style?: CSSProperties;
};
+export type ErrorBoundaryProps = {
+ onReset?: () => void;
+};
+
declare global {
- // eslint-disable-next-line no-var
- var currentBlockSuiteWorkspace: Workspace | undefined;
// eslint-disable-next-line no-var
var currentPage: Page | undefined;
// eslint-disable-next-line no-var
var currentEditor: EditorContainer | undefined;
}
-export const BlockSuiteEditor = (props: EditorProps) => {
+const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
const page = props.page;
+ assertExists(page, 'page should not be null');
const editorRef = useRef(null);
const blockHubRef = useRef(null);
if (editorRef.current === null) {
editorRef.current = new EditorContainer();
- editorRef.current.page = props.page;
- editorRef.current.mode = props.mode;
globalThis.currentEditor = editorRef.current;
}
- const ref = useRef(null);
- useEffect(() => {
- if (editorRef.current) {
- editorRef.current.mode = props.mode;
- }
- }, [props.mode]);
-
- useEffect(() => {
- const editor = editorRef.current;
- if (!editor || !ref.current || !page) {
- return;
- }
-
- editor.page = page;
+ const editor = editorRef.current;
+ assertExists(editorRef, 'editorRef.current should not be null');
+ if (editor.mode !== props.mode) {
+ editor.mode = props.mode;
+ }
+ if (editor.page !== props.page) {
+ editor.page = props.page;
if (page.root === null) {
props.onInit(page, editor);
}
props.onLoad?.(page, editor);
- return;
- }, [page, props]);
+ }
+ const ref = useRef(null);
useEffect(() => {
const editor = editorRef.current;
+ assertExists(editor);
const container = ref.current;
-
- if (!editor || !container || !page) {
+ if (!container) {
return;
}
if (page.awarenessStore.getFlag('enable_block_hub')) {
@@ -82,13 +78,52 @@ export const BlockSuiteEditor = (props: EditorProps) => {
blockHubRef.current?.remove();
container.removeChild(editor);
};
- }, [page, props.mode]);
+ }, [page]);
return (
);
};
+
+const BlockSuiteErrorFallback = (
+ props: FallbackProps & ErrorBoundaryProps
+): ReactElement => {
+ return (
+
+
Sorry.. there was an error
+
{props.error.message}
+
+
+ );
+};
+
+export const BlockSuiteEditor = memo(function BlockSuiteEditor(
+ props: EditorProps & ErrorBoundaryProps
+): ReactElement {
+ return (
+ (
+
+ ),
+ [props.onReset]
+ )}
+ >
+
+
+ );
+});
+
+BlockSuiteEditor.displayName = 'BlockSuiteEditor';
diff --git a/yarn.lock b/yarn.lock
index e0d082033f..f9ba352386 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -77,6 +77,7 @@ __metadata:
react-dnd: ^16.0.1
react-dnd-html5-backend: ^16.0.1
react-dom: ^18.2.0
+ react-error-boundary: ^4.0.3
react-is: ^18.2.0
serve: ^14.2.0
storybook: ^7.0.2
@@ -16017,6 +16018,17 @@ __metadata:
languageName: node
linkType: hard
+"react-error-boundary@npm:^4.0.3":
+ version: 4.0.3
+ resolution: "react-error-boundary@npm:4.0.3"
+ dependencies:
+ "@babel/runtime": ^7.12.5
+ peerDependencies:
+ react: ">=16.13.1"
+ checksum: 50813803d3f03eb2ca07c1250a4a001ffe054f5b3f49b15ea5f0a4e1108f549bc7d8def15db51d518516997d34cebc663f4276246301b4a40a72e83a784f5c38
+ languageName: node
+ linkType: hard
+
"react-i18next@npm:^12.2.0":
version: 12.2.0
resolution: "react-i18next@npm:12.2.0"