Skip to content

Commit c190973

Browse files
committed
refactor: extract actions into memo components with render prop pattern
- Change CommentEditor actions prop from FC to render prop (function returning ReactNode) - Extract inline action components into separate memo-wrapped components - Remove unnecessary useCallback/useMemo wrappers around render props - React.memo on child components handles re-render optimization
1 parent 04d4b6e commit c190973

4 files changed

Lines changed: 215 additions & 155 deletions

File tree

packages/react/src/components/Comments/Comment.tsx

Lines changed: 116 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"use client";
22

3-
import { mergeCSSClasses } from "@blocknote/core";
3+
import { Dictionary, mergeCSSClasses } from "@blocknote/core";
44
import { CommentsExtension } from "@blocknote/core/comments";
55
import type { CommentData, ThreadData } from "@blocknote/core/comments";
6-
import { MouseEvent, ReactNode, useCallback, useState } from "react";
6+
import { ThreadStore } from "@blocknote/core/comments";
7+
import { MouseEvent, ReactNode, memo, useCallback, useState } from "react";
78
import {
89
RiArrowGoBackFill,
910
RiCheckFill,
@@ -13,7 +14,7 @@ import {
1314
RiMoreFill,
1415
} from "react-icons/ri";
1516

16-
import { useComponentsContext } from "../../editor/ComponentsContext.js";
17+
import { Components, useComponentsContext } from "../../editor/ComponentsContext.js";
1718
import { useCreateBlockNote } from "../../hooks/useCreateBlockNote.js";
1819
import { useExtension } from "../../hooks/useExtension.js";
1920
import { useDictionary } from "../../i18n/dictionary.js";
@@ -23,6 +24,103 @@ import { ReactionBadge } from "./ReactionBadge.js";
2324
import { defaultCommentEditorSchema } from "./defaultCommentEditorSchema.js";
2425
import { useUser } from "./useUsers.js";
2526

27+
type CommentEditorActionsProps = {
28+
isFocused: boolean;
29+
isEmpty: boolean;
30+
comment: CommentData;
31+
isEditing: boolean;
32+
threadStore: ThreadStore;
33+
onReactionSelect: (emoji: string) => Promise<void>;
34+
onEditSubmit: (event: MouseEvent) => Promise<void>;
35+
onEditCancel: () => void;
36+
onEmojiPickerOpenChange: (open: boolean) => void;
37+
Components: Components;
38+
dict: Dictionary;
39+
};
40+
41+
const CommentEditorActionsComponent = memo(
42+
({
43+
isEmpty: _isEmpty,
44+
comment,
45+
isEditing,
46+
threadStore,
47+
onReactionSelect,
48+
onEditSubmit,
49+
onEditCancel,
50+
onEmojiPickerOpenChange,
51+
Components,
52+
dict,
53+
}: CommentEditorActionsProps) => {
54+
const canAddReaction = threadStore.auth.canAddReaction(comment);
55+
56+
return (
57+
<>
58+
{comment.reactions.length > 0 && !isEditing && (
59+
<Components.Generic.Badge.Group
60+
className={mergeCSSClasses(
61+
"bn-badge-group",
62+
"bn-comment-reactions",
63+
)}
64+
>
65+
{comment.reactions.map((reaction) => (
66+
<ReactionBadge
67+
key={reaction.emoji}
68+
comment={comment}
69+
emoji={reaction.emoji}
70+
onReactionSelect={onReactionSelect}
71+
/>
72+
))}
73+
{canAddReaction && (
74+
<EmojiPicker
75+
onEmojiSelect={(emoji: { native: string }) =>
76+
onReactionSelect(emoji.native)
77+
}
78+
onOpenChange={onEmojiPickerOpenChange}
79+
>
80+
<Components.Generic.Badge.Root
81+
className={mergeCSSClasses(
82+
"bn-badge",
83+
"bn-comment-add-reaction",
84+
)}
85+
text={"+"}
86+
icon={<RiEmotionLine size={16} />}
87+
mainTooltip={dict.comments.actions.add_reaction}
88+
/>
89+
</EmojiPicker>
90+
)}
91+
</Components.Generic.Badge.Group>
92+
)}
93+
{isEditing && (
94+
<Components.Generic.Toolbar.Root
95+
variant="action-toolbar"
96+
className={mergeCSSClasses(
97+
"bn-action-toolbar",
98+
"bn-comment-actions",
99+
)}
100+
>
101+
<Components.Generic.Toolbar.Button
102+
mainTooltip={dict.comments.save_button_text}
103+
variant="compact"
104+
onClick={onEditSubmit}
105+
isDisabled={_isEmpty}
106+
>
107+
{dict.comments.save_button_text}
108+
</Components.Generic.Toolbar.Button>
109+
<Components.Generic.Toolbar.Button
110+
className={"bn-button"}
111+
mainTooltip={dict.comments.cancel_button_text}
112+
variant="compact"
113+
onClick={onEditCancel}
114+
>
115+
{dict.comments.cancel_button_text}
116+
</Components.Generic.Toolbar.Button>
117+
</Components.Generic.Toolbar.Root>
118+
)}
119+
</>
120+
);
121+
},
122+
);
123+
26124
export type CommentProps = {
27125
comment: CommentData;
28126
thread: ThreadData;
@@ -130,88 +228,6 @@ export const Comment = ({
130228

131229
const user = useUser(comment.userId);
132230

133-
const CommentEditorActions = useCallback(
134-
({ isEmpty }: { isFocused: boolean; isEmpty: boolean }) => {
135-
const canAddReaction = threadStore.auth.canAddReaction(comment);
136-
137-
return (
138-
<>
139-
{comment.reactions.length > 0 && !isEditing && (
140-
<Components.Generic.Badge.Group
141-
className={mergeCSSClasses(
142-
"bn-badge-group",
143-
"bn-comment-reactions",
144-
)}
145-
>
146-
{comment.reactions.map((reaction) => (
147-
<ReactionBadge
148-
key={reaction.emoji}
149-
comment={comment}
150-
emoji={reaction.emoji}
151-
onReactionSelect={onReactionSelect}
152-
/>
153-
))}
154-
{canAddReaction && (
155-
<EmojiPicker
156-
onEmojiSelect={(emoji: { native: string }) =>
157-
onReactionSelect(emoji.native)
158-
}
159-
onOpenChange={setEmojiPickerOpen}
160-
>
161-
<Components.Generic.Badge.Root
162-
className={mergeCSSClasses(
163-
"bn-badge",
164-
"bn-comment-add-reaction",
165-
)}
166-
text={"+"}
167-
icon={<RiEmotionLine size={16} />}
168-
mainTooltip={dict.comments.actions.add_reaction}
169-
/>
170-
</EmojiPicker>
171-
)}
172-
</Components.Generic.Badge.Group>
173-
)}
174-
{isEditing && (
175-
<Components.Generic.Toolbar.Root
176-
variant="action-toolbar"
177-
className={mergeCSSClasses(
178-
"bn-action-toolbar",
179-
"bn-comment-actions",
180-
)}
181-
>
182-
<Components.Generic.Toolbar.Button
183-
mainTooltip={dict.comments.save_button_text}
184-
variant="compact"
185-
onClick={onEditSubmit}
186-
isDisabled={isEmpty}
187-
>
188-
{dict.comments.save_button_text}
189-
</Components.Generic.Toolbar.Button>
190-
<Components.Generic.Toolbar.Button
191-
className={"bn-button"}
192-
mainTooltip={dict.comments.cancel_button_text}
193-
variant="compact"
194-
onClick={onEditCancel}
195-
>
196-
{dict.comments.cancel_button_text}
197-
</Components.Generic.Toolbar.Button>
198-
</Components.Generic.Toolbar.Root>
199-
)}
200-
</>
201-
);
202-
},
203-
[
204-
comment,
205-
isEditing,
206-
threadStore,
207-
onReactionSelect,
208-
onEditSubmit,
209-
onEditCancel,
210-
Components,
211-
dict,
212-
],
213-
);
214-
215231
if (!comment.body) {
216232
return null;
217233
}
@@ -331,7 +347,21 @@ export const Comment = ({
331347
editable={isEditing}
332348
actions={
333349
comment.reactions.length > 0 || isEditing
334-
? CommentEditorActions
350+
? ({ isFocused, isEmpty }) => (
351+
<CommentEditorActionsComponent
352+
isFocused={isFocused}
353+
isEmpty={isEmpty}
354+
comment={comment}
355+
isEditing={isEditing}
356+
threadStore={threadStore}
357+
onReactionSelect={onReactionSelect}
358+
onEditSubmit={onEditSubmit}
359+
onEditCancel={onEditCancel}
360+
onEmojiPickerOpenChange={setEmojiPickerOpen}
361+
Components={Components}
362+
dict={dict}
363+
/>
364+
)
335365
: undefined
336366
}
337367
/>

packages/react/src/components/Comments/CommentEditor.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BlockNoteEditor } from "@blocknote/core";
2-
import { FC, useCallback, useEffect, useState } from "react";
2+
import { ReactNode, useCallback, useEffect, useState } from "react";
33
import { useComponentsContext } from "../../editor/ComponentsContext.js";
44
import { useEditorState } from "../../hooks/useEditorState.js";
55

@@ -16,10 +16,7 @@ import { useEditorState } from "../../hooks/useEditorState.js";
1616
export const CommentEditor = (props: {
1717
autoFocus?: boolean;
1818
editable: boolean;
19-
actions?: FC<{
20-
isFocused: boolean;
21-
isEmpty: boolean;
22-
}>;
19+
actions?: (args: { isFocused: boolean; isEmpty: boolean }) => ReactNode;
2320
editor: BlockNoteEditor<any, any, any>;
2421
}) => {
2522
const [isFocused, setIsFocused] = useState(false);
@@ -58,7 +55,7 @@ export const CommentEditor = (props: {
5855
/>
5956
{props.actions && (
6057
<div className={"bn-comment-actions-wrapper"}>
61-
<props.actions isFocused={isFocused} isEmpty={isEmpty} />
58+
{props.actions({ isFocused, isEmpty })}
6259
</div>
6360
)}
6461
</>

packages/react/src/components/Comments/FloatingComposer.tsx

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import {
33
DefaultBlockSchema,
44
DefaultInlineContentSchema,
55
DefaultStyleSchema,
6+
Dictionary,
67
InlineContentSchema,
78
mergeCSSClasses,
89
StyleSchema,
910
} from "@blocknote/core";
1011
import { CommentsExtension } from "@blocknote/core/comments";
11-
import { useCallback } from "react";
12+
import { memo, useCallback } from "react";
1213

13-
import { useComponentsContext } from "../../editor/ComponentsContext.js";
14+
import { Components, useComponentsContext } from "../../editor/ComponentsContext.js";
1415
import { useCreateBlockNote } from "../../hooks/useCreateBlockNote.js";
1516
import { useExtension } from "../../hooks/useExtension.js";
1617
import { useDictionary } from "../../i18n/dictionary.js";
@@ -19,6 +20,33 @@ import { defaultCommentEditorSchema } from "./defaultCommentEditorSchema.js";
1920
import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
2021
import { TextSelection } from "@tiptap/pm/state";
2122

23+
type FloatingComposerActionsProps = {
24+
isFocused: boolean;
25+
isEmpty: boolean;
26+
onSave: () => Promise<void>;
27+
Components: Components;
28+
dict: Dictionary;
29+
};
30+
31+
const FloatingComposerActionsComponent = memo(
32+
({ isEmpty, onSave, Components, dict }: FloatingComposerActionsProps) => (
33+
<Components.Generic.Toolbar.Root
34+
className={mergeCSSClasses("bn-action-toolbar", "bn-comment-actions")}
35+
variant="action-toolbar"
36+
>
37+
<Components.Generic.Toolbar.Button
38+
className={"bn-button"}
39+
mainTooltip={dict.comments.save_button_text}
40+
variant="compact"
41+
isDisabled={isEmpty}
42+
onClick={onSave}
43+
>
44+
{dict.comments.save_button_text}
45+
</Components.Generic.Toolbar.Button>
46+
</Components.Generic.Toolbar.Root>
47+
),
48+
);
49+
2250
/**
2351
* The FloatingComposer component displays a comment editor "floating" card.
2452
*
@@ -47,45 +75,35 @@ export function FloatingComposer<
4775
schema: comments.commentEditorSchema || defaultCommentEditorSchema,
4876
});
4977

50-
const Actions = useCallback(
51-
({ isEmpty }: { isFocused: boolean; isEmpty: boolean }) => (
52-
<Components.Generic.Toolbar.Root
53-
className={mergeCSSClasses("bn-action-toolbar", "bn-comment-actions")}
54-
variant="action-toolbar"
55-
>
56-
<Components.Generic.Toolbar.Button
57-
className={"bn-button"}
58-
mainTooltip={dict.comments.save_button_text}
59-
variant="compact"
60-
isDisabled={isEmpty}
61-
onClick={async () => {
62-
// (later) For REST API, we should implement a loading state and error state
63-
await comments.createThread({
64-
initialComment: {
65-
body: newCommentEditor.document,
66-
},
67-
});
68-
comments.stopPendingComment();
69-
editor.transact((tr) => {
70-
tr.setSelection(TextSelection.create(tr.doc, tr.selection.to));
71-
});
72-
editor.focus();
73-
}}
74-
>
75-
{dict.comments.save_button_text}
76-
</Components.Generic.Toolbar.Button>
77-
</Components.Generic.Toolbar.Root>
78-
),
79-
[Components, dict, comments, newCommentEditor, editor],
80-
);
78+
const onSave = useCallback(async () => {
79+
// (later) For REST API, we should implement a loading state and error state
80+
await comments.createThread({
81+
initialComment: {
82+
body: newCommentEditor.document,
83+
},
84+
});
85+
comments.stopPendingComment();
86+
editor.transact((tr) => {
87+
tr.setSelection(TextSelection.create(tr.doc, tr.selection.to));
88+
});
89+
editor.focus();
90+
}, [comments, newCommentEditor, editor]);
8191

8292
return (
8393
<Components.Comments.Card className={"bn-thread"}>
8494
<CommentEditor
8595
autoFocus={true}
8696
editable={true}
8797
editor={newCommentEditor}
88-
actions={Actions}
98+
actions={({ isFocused, isEmpty }) => (
99+
<FloatingComposerActionsComponent
100+
isFocused={isFocused}
101+
isEmpty={isEmpty}
102+
onSave={onSave}
103+
Components={Components}
104+
dict={dict}
105+
/>
106+
)}
89107
/>
90108
</Components.Comments.Card>
91109
);

0 commit comments

Comments
 (0)