-
diff --git a/packages/react/src/components/BubbleMenu.tsx b/packages/react/src/components/BubbleMenu.tsx
index 2798d61..48ffad7 100644
--- a/packages/react/src/components/BubbleMenu.tsx
+++ b/packages/react/src/components/BubbleMenu.tsx
@@ -36,9 +36,9 @@ import { ColorPicker } from './ColorPicker';
*/
export interface BubbleMenuProps {
/**
- * The OpenBlockEditor instance.
+ * The OpenBlockEditor instance (can be null during initialization).
*/
- editor: OpenBlockEditor;
+ editor: OpenBlockEditor | null;
/**
* Custom render function for the menu content.
@@ -402,6 +402,8 @@ export function BubbleMenu({
const linkButtonRef = useRef
(null);
useEffect(() => {
+ if (!editor || editor.isDestroyed) return;
+
const updateState = () => {
const state = BUBBLE_MENU_PLUGIN_KEY.getState(editor.pm.state);
setMenuState(state ?? null);
@@ -421,26 +423,31 @@ export function BubbleMenu({
}, [menuState?.visible]);
const toggleBold = useCallback(() => {
+ if (!editor || editor.isDestroyed) return;
editor.toggleBold();
editor.pm.view.focus();
}, [editor]);
const toggleItalic = useCallback(() => {
+ if (!editor || editor.isDestroyed) return;
editor.toggleItalic();
editor.pm.view.focus();
}, [editor]);
const toggleUnderline = useCallback(() => {
+ if (!editor || editor.isDestroyed) return;
editor.toggleUnderline();
editor.pm.view.focus();
}, [editor]);
const toggleStrikethrough = useCallback(() => {
+ if (!editor || editor.isDestroyed) return;
editor.toggleStrikethrough();
editor.pm.view.focus();
}, [editor]);
const toggleCode = useCallback(() => {
+ if (!editor || editor.isDestroyed) return;
editor.toggleCode();
editor.pm.view.focus();
}, [editor]);
@@ -453,7 +460,7 @@ export function BubbleMenu({
setShowLinkPopover(false);
}, []);
- if (!menuState?.visible || !menuState.coords) {
+ if (!editor || editor.isDestroyed || !menuState?.visible || !menuState.coords) {
return null;
}
diff --git a/packages/react/src/components/OpenBlockView.tsx b/packages/react/src/components/OpenBlockView.tsx
index e7940fe..bba621b 100644
--- a/packages/react/src/components/OpenBlockView.tsx
+++ b/packages/react/src/components/OpenBlockView.tsx
@@ -28,9 +28,9 @@ import { OpenBlockEditor } from '@labbs/openblock-core';
*/
export interface OpenBlockViewProps {
/**
- * The OpenBlockEditor instance to render
+ * The OpenBlockEditor instance to render (can be null during initialization)
*/
- editor: OpenBlockEditor;
+ editor: OpenBlockEditor | null;
/**
* Additional class name(s) for the container
@@ -58,9 +58,9 @@ export interface OpenBlockViewRef {
container: HTMLDivElement | null;
/**
- * The OpenBlockEditor instance
+ * The OpenBlockEditor instance (can be null during initialization)
*/
- editor: OpenBlockEditor;
+ editor: OpenBlockEditor | null;
}
/**
diff --git a/packages/react/src/components/SlashMenu.tsx b/packages/react/src/components/SlashMenu.tsx
index 440f436..a7c0687 100644
--- a/packages/react/src/components/SlashMenu.tsx
+++ b/packages/react/src/components/SlashMenu.tsx
@@ -37,9 +37,9 @@ import {
*/
export interface SlashMenuProps {
/**
- * The OpenBlockEditor instance.
+ * The OpenBlockEditor instance (can be null during initialization).
*/
- editor: OpenBlockEditor;
+ editor: OpenBlockEditor | null;
/**
* Custom menu items (optional).
@@ -121,12 +121,14 @@ export function SlashMenu({
const [openUpward, setOpenUpward] = useState(false);
const menuRef = useRef(null);
- // Get menu items
- const allItems = customItems ?? getDefaultSlashMenuItems(editor.pm.state.schema);
+ // Get menu items (only when editor is available)
+ const allItems = editor ? (customItems ?? getDefaultSlashMenuItems(editor.pm.state.schema)) : [];
const filteredItems = menuState ? filterSlashMenuItems(allItems, menuState.query) : [];
// Subscribe to plugin state changes
useEffect(() => {
+ if (!editor || editor.isDestroyed) return;
+
const updateState = () => {
const state = SLASH_MENU_PLUGIN_KEY.getState(editor.pm.state);
setMenuState(state ?? null);
@@ -143,7 +145,7 @@ export function SlashMenu({
// Handle keyboard navigation
useEffect(() => {
- if (!menuState?.active) return;
+ if (!editor || editor.isDestroyed || !menuState?.active) return;
const handleKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
@@ -175,7 +177,7 @@ export function SlashMenu({
// Handle item selection
const handleSelect = useCallback(
(item: SlashMenuItem) => {
- if (!menuState) return;
+ if (!editor || editor.isDestroyed || !menuState) return;
executeSlashCommand(editor.pm.view, menuState, item.action);
editor.pm.view.focus();
},
diff --git a/packages/react/src/components/TableHandles.tsx b/packages/react/src/components/TableHandles.tsx
index 9e567a1..ba4a562 100644
--- a/packages/react/src/components/TableHandles.tsx
+++ b/packages/react/src/components/TableHandles.tsx
@@ -36,9 +36,9 @@ import {
*/
export interface TableHandlesProps {
/**
- * The OpenBlockEditor instance.
+ * The OpenBlockEditor instance (can be null during initialization).
*/
- editor: OpenBlockEditor;
+ editor: OpenBlockEditor | null;
/**
* Additional class name for the handles container.
@@ -144,6 +144,8 @@ export function TableHandles({
// Track mouse position to detect which row/col is hovered
useEffect(() => {
+ if (!editor || editor.isDestroyed) return;
+
let hideTimeout: ReturnType | null = null;
const clearHideTimeout = () => {
@@ -286,7 +288,7 @@ export function TableHandles({
const handleAddRow = useCallback(
(index: number) => {
- if (!tableState) return;
+ if (!editor || editor.isDestroyed || !tableState) return;
addRowAtIndex(editor.pm.state, editor.pm.view.dispatch, tableState.tablePos, index);
editor.pm.view.focus();
setShowRowMenu(null);
@@ -296,7 +298,7 @@ export function TableHandles({
const handleDeleteRow = useCallback(
(index: number) => {
- if (!tableState) return;
+ if (!editor || editor.isDestroyed || !tableState) return;
deleteRowAtIndex(editor.pm.state, editor.pm.view.dispatch, tableState.tablePos, index);
editor.pm.view.focus();
setShowRowMenu(null);
@@ -306,7 +308,7 @@ export function TableHandles({
const handleAddCol = useCallback(
(index: number) => {
- if (!tableState) return;
+ if (!editor || editor.isDestroyed || !tableState) return;
addColumnAtIndex(editor.pm.state, editor.pm.view.dispatch, tableState.tablePos, index);
editor.pm.view.focus();
setShowColMenu(null);
@@ -316,7 +318,7 @@ export function TableHandles({
const handleDeleteCol = useCallback(
(index: number) => {
- if (!tableState) return;
+ if (!editor || editor.isDestroyed || !tableState) return;
deleteColumnAtIndex(editor.pm.state, editor.pm.view.dispatch, tableState.tablePos, index);
editor.pm.view.focus();
setShowColMenu(null);
@@ -324,7 +326,7 @@ export function TableHandles({
[editor, tableState]
);
- if (!tableState) return null;
+ if (!editor || editor.isDestroyed || !tableState) return null;
const tableRect = tableState.tableElement.getBoundingClientRect();
diff --git a/packages/react/src/components/TableMenu.tsx b/packages/react/src/components/TableMenu.tsx
index 4bbbb4b..b43a357 100644
--- a/packages/react/src/components/TableMenu.tsx
+++ b/packages/react/src/components/TableMenu.tsx
@@ -39,9 +39,9 @@ import {
*/
export interface TableMenuProps {
/**
- * The OpenBlockEditor instance.
+ * The OpenBlockEditor instance (can be null during initialization).
*/
- editor: OpenBlockEditor;
+ editor: OpenBlockEditor | null;
/**
* Additional class name for the menu container.
@@ -207,6 +207,8 @@ export function TableMenu({
const [coords, setCoords] = useState<{ left: number; top: number } | null>(null);
useEffect(() => {
+ if (!editor || editor.isDestroyed) return;
+
const updateState = () => {
const state = editor.pm.state;
const isInsideTable = isInTable(state);
@@ -247,41 +249,48 @@ export function TableMenu({
}, [editor]);
const handleAddRowBefore = useCallback(() => {
+ if (!editor || editor.isDestroyed) return;
addRowBefore(editor.pm.state, editor.pm.view.dispatch);
editor.pm.view.focus();
}, [editor]);
const handleAddRowAfter = useCallback(() => {
+ if (!editor || editor.isDestroyed) return;
addRowAfter(editor.pm.state, editor.pm.view.dispatch);
editor.pm.view.focus();
}, [editor]);
const handleDeleteRow = useCallback(() => {
+ if (!editor || editor.isDestroyed) return;
deleteRow(editor.pm.state, editor.pm.view.dispatch);
editor.pm.view.focus();
}, [editor]);
const handleAddColumnBefore = useCallback(() => {
+ if (!editor || editor.isDestroyed) return;
addColumnBefore(editor.pm.state, editor.pm.view.dispatch);
editor.pm.view.focus();
}, [editor]);
const handleAddColumnAfter = useCallback(() => {
+ if (!editor || editor.isDestroyed) return;
addColumnAfter(editor.pm.state, editor.pm.view.dispatch);
editor.pm.view.focus();
}, [editor]);
const handleDeleteColumn = useCallback(() => {
+ if (!editor || editor.isDestroyed) return;
deleteColumn(editor.pm.state, editor.pm.view.dispatch);
editor.pm.view.focus();
}, [editor]);
const handleDeleteTable = useCallback(() => {
+ if (!editor || editor.isDestroyed) return;
deleteTable(editor.pm.state, editor.pm.view.dispatch);
editor.pm.view.focus();
}, [editor]);
- if (!inTable || !coords) {
+ if (!editor || editor.isDestroyed || !inTable || !coords) {
return null;
}
diff --git a/packages/react/src/hooks/useOpenBlock.ts b/packages/react/src/hooks/useOpenBlock.ts
index 1b9bdcb..c2e9f39 100644
--- a/packages/react/src/hooks/useOpenBlock.ts
+++ b/packages/react/src/hooks/useOpenBlock.ts
@@ -19,7 +19,7 @@
* ```
*/
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { OpenBlockEditor, EditorConfig, Block } from '@labbs/openblock-core';
/**
@@ -31,21 +31,25 @@ export interface UseOpenBlockOptions extends Omit {}
* Create and manage an OpenBlockEditor instance
*
* @param options - Editor configuration options
- * @returns The OpenBlockEditor instance
+ * @returns The OpenBlockEditor instance, or null during initialization
+ *
+ * @remarks
+ * This hook properly handles React 18+ StrictMode, which mounts components twice
+ * in development. The editor is created in useEffect to ensure a fresh instance
+ * is created after each mount/unmount cycle.
*/
-export function useOpenBlock(options: UseOpenBlockOptions = {}): OpenBlockEditor {
- // Use useState with lazy initializer to create the editor exactly once
- // This is React 18+ safe and works with StrictMode
- const [editor] = useState(() => new OpenBlockEditor(options));
+export function useOpenBlock(options: UseOpenBlockOptions = {}): OpenBlockEditor | null {
+ const [editor, setEditor] = useState(null);
+ const optionsRef = useRef(options);
- // Cleanup on unmount only
useEffect(() => {
+ const newEditor = new OpenBlockEditor(optionsRef.current);
+ setEditor(newEditor);
+
return () => {
- if (!editor.isDestroyed) {
- editor.destroy();
- }
+ newEditor.destroy();
};
- }, [editor]);
+ }, []);
return editor;
}