perf: Optimize component rendering with memoization and useCallback hooks (#5253)

*  (NodeDescription/index.tsx): Memoize Markdown component and description rendering for performance optimization
♻️ (NodeOutputfield/index.tsx): Memoize HandleRenderComponent and Handle instance with useMemo and memo for performance optimization
 (RenderInputParameters/index.tsx): Add new component RenderInputParameters to render input parameters with memoization for improved performance

📝 (GenericNode/index.tsx): Remove unused imports and functions, refactor sortToolModeFields to be exported, and optimize key generation and memoization for NodeOutputField component
📝 (flowSidebarComponent/index.tsx): Refactor event handlers to use useCallback for better performance
📝 (nodeToolbarComponent/index.tsx): Refactor event handlers to use useCallback for better performance and readability

* ♻️ (NodeDescription/index.tsx): Remove unnecessary comments and improve code readability by removing redundant comments
♻️ (NodeOutputfield/index.tsx): Remove unnecessary comments and improve code readability by removing redundant comments
♻️ (RenderInputParameters/index.tsx): Remove unnecessary comments and improve code readability by removing redundant comments
♻️ (index.tsx): Remove unnecessary comments and improve code readability by removing redundant comments

*  (decisionFlow.spec.ts): add @workflow tag to the test case for better categorization and organization.

* 📝 (GenericNode/index.tsx): Organize imports and update component import for better code structure and readability
📝 (GenericNode/index.tsx): Refactor renderOutputs function to improve code readability and maintainability
📝 (GenericNode/index.tsx): Refactor renderOutputParameter function to use OutputParameter component for consistency
📝 (GenericNode/index.tsx): Refactor output rendering logic to use OutputParameter component for better code structure
📝 (sidebarDraggableComponent/index.tsx): Update logic to remove cursor element only if it exists to prevent errors

*  (NodeOutputParameter/index.tsx): Add a new component for rendering output parameters in the frontend to improve modularity and reusability.
This commit is contained in:
Cristhian Zanforlin Lousa 2024-12-13 19:24:45 -03:00 committed by GitHub
commit 977ba926c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 372 additions and 200 deletions

View file

@ -3,7 +3,7 @@ import useFlowsManagerStore from "@/stores/flowsManagerStore";
import useFlowStore from "@/stores/flowStore";
import { handleKeyDown } from "@/utils/reactflowUtils";
import { cn } from "@/utils/utils";
import { useEffect, useRef, useState } from "react";
import { memo, useEffect, useMemo, useRef, useState } from "react";
import Markdown from "react-markdown";
export default function NodeDescription({
@ -59,6 +59,25 @@ export default function NodeDescription({
setNodeDescription(description);
}, [description]);
const MemoizedMarkdown = memo(Markdown);
const renderedDescription = useMemo(
() =>
description === "" || !description ? (
emptyPlaceholder
) : (
<MemoizedMarkdown
linkTarget="_blank"
className={cn(
"markdown prose flex w-full flex-col text-[13px] leading-5 word-break-break-word [&_pre]:whitespace-break-spaces [&_pre]:!bg-code-description-background [&_pre_code]:!bg-code-description-background",
mdClassName,
)}
>
{String(description)}
</MemoizedMarkdown>
),
[description, emptyPlaceholder, mdClassName],
);
return (
<div
className={cn(
@ -138,19 +157,7 @@ export default function NodeDescription({
takeSnapshot();
}}
>
{description === "" || !description ? (
emptyPlaceholder
) : (
<Markdown
linkTarget="_blank"
className={cn(
"markdown prose flex w-full flex-col text-[13px] leading-5 word-break-break-word [&_pre]:whitespace-break-spaces [&_pre]:!bg-code-description-background [&_pre_code]:!bg-code-description-background",
mdClassName,
)}
>
{String(description)}
</Markdown>
)}
{renderedDescription}
</div>
)}
</div>

View file

@ -0,0 +1,56 @@
import React, { useMemo } from "react";
import { getNodeOutputColors } from "../../../helpers/get-node-output-colors";
import { getNodeOutputColorsName } from "../../../helpers/get-node-output-colors-name";
import NodeOutputField from "../NodeOutputfield";
export const OutputParameter = ({
output,
idx,
lastOutput,
data,
types,
selected,
showNode,
isToolMode,
}) => {
const id = useMemo(
() => ({
output_types: [output.selected ?? output.types[0]],
id: data.id,
dataType: data.type,
name: output.name,
}),
[output.selected, output.types, data.id, data.type, output.name],
);
const colors = useMemo(
() => getNodeOutputColors(output, data, types),
[output, data.type, data.id, types],
);
const colorNames = useMemo(
() => getNodeOutputColorsName(output, data, types),
[output, data.type, data.id, types],
);
return (
<NodeOutputField
index={idx}
lastOutput={lastOutput}
selected={selected}
key={output.name + idx}
data={data}
colors={colors}
outputProxy={output.proxy}
title={output.display_name ?? output.name}
tooltipTitle={output.selected ?? output.types[0]}
id={id}
type={output.types.join("|")}
showNode={showNode}
outputName={output.name}
colorName={colorNames}
isToolMode={isToolMode}
/>
);
};

View file

@ -1,6 +1,6 @@
import { ICON_STROKE_WIDTH } from "@/constants/constants";
import { cloneDeep } from "lodash";
import { useEffect, useRef } from "react";
import { memo, useEffect, useMemo, useRef } from "react";
import { useUpdateNodeInternals } from "reactflow";
import { default as IconComponent } from "../../../../components/common/genericIconComponent";
import ShadTooltip from "../../../../components/common/shadTooltipComponent";
@ -105,22 +105,52 @@ export default function NodeOutputField({
}
}, [disabledOutput]);
const Handle = (
<HandleRenderComponent
left={false}
nodes={nodes}
tooltipTitle={tooltipTitle}
id={id}
title={title}
edges={edges}
nodeId={data.id}
myData={myData}
colors={colors}
setFilterEdge={setFilterEdge}
showNode={showNode}
testIdComplement={`${data?.type?.toLowerCase()}-${showNode ? "shownode" : "noshownode"}`}
colorName={colorName}
/>
const MemoizedHandleRenderComponent = memo(
HandleRenderComponent,
(prev, next) => {
return (
prev.nodeId === next.nodeId &&
prev.myData === next.myData &&
prev.showNode === next.showNode &&
prev.tooltipTitle === next.tooltipTitle &&
prev.colors === next.colors &&
prev.colorName === next.colorName
);
},
);
const Handle = useMemo(
() => (
<MemoizedHandleRenderComponent
left={false}
nodes={nodes}
tooltipTitle={tooltipTitle}
id={id}
title={title}
edges={edges}
nodeId={data.id}
myData={myData}
colors={colors}
setFilterEdge={setFilterEdge}
showNode={showNode}
testIdComplement={`${data?.type?.toLowerCase()}-${showNode ? "shownode" : "noshownode"}`}
colorName={colorName}
/>
),
[
nodes,
tooltipTitle,
id,
title,
edges,
data.id,
myData,
colors,
setFilterEdge,
showNode,
data?.type,
colorName,
],
);
return !showNode ? (

View file

@ -0,0 +1,122 @@
import { getNodeInputColors } from "@/CustomNodes/helpers/get-node-input-colors";
import { getNodeInputColorsName } from "@/CustomNodes/helpers/get-node-input-colors-name";
import getFieldTitle from "@/CustomNodes/utils/get-field-title";
import { scapedJSONStringfy } from "@/utils/reactflowUtils";
import { useMemo } from "react";
import { sortToolModeFields } from "../..";
import NodeInputField from "../NodeInputField";
const RenderInputParameters = ({
data,
types,
isToolMode,
showNode,
shownOutputs,
showHiddenOutputs,
}) => {
const templateFields = useMemo(() => {
return Object.keys(data.node?.template || {})
.filter((templateField) => templateField.charAt(0) !== "_")
.sort((a, b) =>
sortToolModeFields(
a,
b,
data.node!.template,
data.node?.field_order ?? [],
isToolMode,
),
);
}, [data.node?.template, data.node?.field_order, isToolMode]);
const memoizedColors = useMemo(() => {
const colorMap = new Map();
templateFields.forEach((templateField) => {
const template = data.node?.template[templateField];
if (template) {
colorMap.set(templateField, {
colors: getNodeInputColors(
template.input_types,
template.type,
types,
),
colorsName: getNodeInputColorsName(
template.input_types,
template.type,
types,
),
});
}
});
return colorMap;
}, [templateFields, types, data.node?.template]);
const memoizedKeys = useMemo(() => {
const keyMap = new Map();
templateFields.forEach((templateField) => {
const template = data.node?.template[templateField];
if (template) {
keyMap.set(
templateField,
scapedJSONStringfy({
inputTypes: template.input_types,
type: template.type,
id: data.id,
fieldName: templateField,
proxy: template.proxy,
}),
);
}
});
return keyMap;
}, [templateFields, data.id, data.node?.template]);
const renderInputParameter = templateFields.map(
(templateField: string, idx) => {
const template = data.node?.template[templateField];
if (!template?.show || template?.advanced) {
return null;
}
const memoizedColor = memoizedColors.get(templateField);
const memoizedKey = memoizedKeys.get(templateField);
return (
<NodeInputField
lastInput={
idx === templateFields.length - 1 &&
!(shownOutputs.length > 0 || showHiddenOutputs)
}
key={memoizedKey}
data={data}
colors={memoizedColor.colors}
title={getFieldTitle(data.node?.template!, templateField)}
info={template.info!}
name={templateField}
tooltipTitle={template.input_types?.join("\n") ?? template.type}
required={template.required}
id={{
inputTypes: template.input_types,
type: template.type,
id: data.id,
fieldName: templateField,
}}
type={template.type}
optionalHandle={template.input_types}
proxy={template.proxy}
showNode={showNode}
colorName={memoizedColor.colorsName}
isToolMode={isToolMode && template.tool_mode}
/>
);
},
);
return <>{renderInputParameter}</>;
};
export default RenderInputParameters;

View file

@ -17,29 +17,22 @@ import { useShortcutsStore } from "../../stores/shortcuts";
import { useTypesStore } from "../../stores/typesStore";
import { OutputFieldType, VertexBuildTypeAPI } from "../../types/api";
import { NodeDataType } from "../../types/flow";
import {
checkHasToolMode,
scapedJSONStringfy,
} from "../../utils/reactflowUtils";
import { checkHasToolMode } from "../../utils/reactflowUtils";
import { classNames, cn } from "../../utils/utils";
import { getNodeInputColors } from "../helpers/get-node-input-colors";
import { getNodeInputColorsName } from "../helpers/get-node-input-colors-name";
import { getNodeOutputColors } from "../helpers/get-node-output-colors";
import { getNodeOutputColorsName } from "../helpers/get-node-output-colors-name";
import { processNodeAdvancedFields } from "../helpers/process-node-advanced-fields";
import useCheckCodeValidity from "../hooks/use-check-code-validity";
import useUpdateNodeCode from "../hooks/use-update-node-code";
import getFieldTitle from "../utils/get-field-title";
import sortFields from "../utils/sort-fields";
import NodeDescription from "./components/NodeDescription";
import NodeInputField from "./components/NodeInputField";
import NodeName from "./components/NodeName";
import NodeOutputField from "./components/NodeOutputfield";
import { OutputParameter } from "./components/NodeOutputParameter";
import NodeStatus from "./components/NodeStatus";
import RenderInputParameters from "./components/RenderInputParameters";
import { NodeIcon } from "./components/nodeIcon";
import { useBuildStatus } from "./hooks/use-get-build-status";
const sortToolModeFields = (
export const sortToolModeFields = (
a: string,
b: string,
template: any,
@ -172,42 +165,23 @@ export default function GenericNode({
const [openShowMoreOptions, setOpenShowMoreOptions] = useState(false);
const renderOutputParameter = (
output: OutputFieldType,
idx: number,
lastOutput: boolean,
) => {
return (
<NodeOutputField
index={idx}
lastOutput={lastOutput}
selected={selected}
key={
scapedJSONStringfy({
output_types: output.types,
name: output.name,
id: data.id,
dataType: data.type,
}) + idx
const renderOutputs = (outputs) => {
return outputs.map((output, idx) => (
<OutputParameter
key={output.name + idx}
output={output}
idx={
data.node!.outputs?.findIndex((out) => out.name === output.name) ??
idx
}
lastOutput={idx === outputs.length - 1}
data={data}
colors={getNodeOutputColors(output, data, types)}
outputProxy={output.proxy}
title={output.display_name ?? output.name}
tooltipTitle={output.selected ?? output.types[0]}
id={{
output_types: [output.selected ?? output.types[0]],
id: data.id,
dataType: data.type,
name: output.name,
}}
type={output.types.join("|")}
types={types}
selected={selected}
showNode={showNode}
outputName={output.name}
colorName={getNodeOutputColorsName(output, data, types)}
isToolMode={isToolMode}
/>
);
));
};
useEffect(() => {
@ -260,72 +234,6 @@ export default function GenericNode({
data.node?.outputs?.some((output) => output.name === "component_as_tool") ??
false;
const renderInputParameter = Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.sort((a, b) =>
sortToolModeFields(
a,
b,
data.node!.template,
data.node?.field_order ?? [],
isToolMode,
),
)
.map(
(templateField: string, idx) =>
data.node!.template[templateField]?.show &&
!data.node!.template[templateField]?.advanced && (
<NodeInputField
lastInput={
idx ===
Object.keys(data.node!.template).filter(
(templateField) => templateField.charAt(0) !== "_",
).length -
1 && !(shownOutputs.length > 0 || showHiddenOutputs)
}
key={scapedJSONStringfy({
inputTypes: data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
proxy: data.node!.template[templateField].proxy,
})}
data={data}
colors={getNodeInputColors(
data.node?.template[templateField].input_types,
data.node?.template[templateField].type,
types,
)}
title={getFieldTitle(data.node?.template!, templateField)}
info={data.node?.template[templateField].info!}
name={templateField}
tooltipTitle={
data.node?.template[templateField].input_types?.join("\n") ??
data.node?.template[templateField].type
}
required={data.node!.template[templateField].required}
id={{
inputTypes: data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
}}
type={data.node?.template[templateField].type}
optionalHandle={data.node?.template[templateField].input_types}
proxy={data.node?.template[templateField].proxy}
showNode={showNode}
colorName={getNodeInputColorsName(
data.node?.template[templateField].input_types,
data.node?.template[templateField].type,
types,
)}
isToolMode={
isToolMode && data.node!.template[templateField].tool_mode
}
/>
),
);
const buildStatus = useBuildStatus(data, data.id);
const hasOutputs = data.node?.outputs && data.node?.outputs.length > 0;
const [validationStatus, setValidationStatus] =
@ -412,16 +320,17 @@ export default function GenericNode({
<div>
{!showNode && (
<>
{renderInputParameter}
<RenderInputParameters
data={data}
types={types}
isToolMode={isToolMode}
showNode={showNode}
shownOutputs={shownOutputs}
showHiddenOutputs={showHiddenOutputs}
/>
{shownOutputs &&
shownOutputs.length > 0 &&
renderOutputParameter(
shownOutputs[0],
data.node!.outputs?.findIndex(
(out) => out.name === shownOutputs[0].name,
) ?? 0,
false,
)}
renderOutputs(shownOutputs)}
</>
)}
</div>
@ -452,10 +361,15 @@ export default function GenericNode({
</div>
{showNode && (
<div className="relative">
{/* increase height!! */}
<>
{renderInputParameter}
<RenderInputParameters
data={data}
types={types}
isToolMode={isToolMode}
showNode={showNode}
shownOutputs={shownOutputs}
showHiddenOutputs={showHiddenOutputs}
/>
<div
className={classNames(
Object.keys(data.node!.template).length < 1 ? "hidden" : "",
@ -466,29 +380,44 @@ export default function GenericNode({
</div>
{!showHiddenOutputs &&
shownOutputs &&
shownOutputs.map((output, idx) =>
renderOutputParameter(
output,
data.node!.outputs?.findIndex(
(out) => out.name === output.name,
) ?? idx,
idx === shownOutputs.length - 1,
),
)}
shownOutputs.map((output, idx) => (
<OutputParameter
key={`shown-${output.name}-${idx}`}
output={output}
idx={
data.node!.outputs?.findIndex(
(out) => out.name === output.name,
) ?? idx
}
lastOutput={idx === shownOutputs.length - 1}
data={data}
types={types}
selected={selected}
showNode={showNode}
isToolMode={isToolMode}
/>
))}
<div
className={cn(showHiddenOutputs ? "" : "h-0 overflow-hidden")}
>
<div className="block">
{data.node!.outputs &&
data.node!.outputs.map((output, idx) => {
return renderOutputParameter(
output,
{data.node!.outputs?.map((output, idx) => (
<OutputParameter
key={`hidden-${output.name}-${idx}`}
output={output}
idx={
data.node!.outputs?.findIndex(
(out) => out.name === output.name,
) ?? idx,
idx === (data.node!.outputs?.length ?? 0) - 1,
);
})}
) ?? idx
}
lastOutput={idx === (data.node!.outputs?.length ?? 0) - 1}
data={data}
types={types}
selected={selected}
showNode={showNode}
isToolMode={isToolMode}
/>
))}
</div>
</div>
{hiddenOutputs && hiddenOutputs.length > 0 && (

View file

@ -140,9 +140,13 @@ export const SidebarDraggableComponent = forwardRef(
}}
onDragStart={onDragStart}
onDragEnd={() => {
document.body.removeChild(
document.getElementsByClassName("cursor-grabbing")[0],
);
if (
document.getElementsByClassName("cursor-grabbing").length > 0
) {
document.body.removeChild(
document.getElementsByClassName("cursor-grabbing")[0],
);
}
}}
>
<ForwardedIconComponent

View file

@ -1,5 +1,5 @@
import Fuse from "fuse.js";
import { useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook"; // Import useHotkeys
import ForwardedIconComponent from "@/components/common/genericIconComponent";
@ -289,6 +289,27 @@ export function FlowSidebarComponent() {
const nodes = useFlowStore((state) => state.nodes);
const chatInputAdded = checkChatInput(nodes);
const handleInputFocus = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
setIsInputFocused(true);
},
[],
);
const handleInputBlur = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
setIsInputFocused(false);
},
[],
);
const handleInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
handleSearchInput(event.target.value);
},
[],
);
return (
<Sidebar
collapsible="offcanvas"
@ -339,10 +360,10 @@ export function FlowSidebarComponent() {
data-testid="sidebar-search-input"
className="w-full rounded-lg bg-background pl-8 text-sm"
placeholder=""
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
onChange={handleInputChange}
value={search}
onChange={(e) => handleSearchInput(e.target.value)}
/>
{!isInputFocused && search === "" && (
<div className="pointer-events-none absolute inset-y-0 left-8 top-1/2 flex w-4/5 -translate-y-1/2 items-center justify-between gap-2 text-sm text-muted-foreground">

View file

@ -12,7 +12,7 @@ import useAddFlow from "@/hooks/flows/use-add-flow";
import CodeAreaModal from "@/modals/codeAreaModal";
import { APIClassType } from "@/types/api";
import _, { cloneDeep } from "lodash";
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useUpdateNodeInternals } from "reactflow";
import IconComponent from "../../../../components/common/genericIconComponent";
import {
@ -395,6 +395,28 @@ export default function NodeToolbarComponent({
tool_mode: data.node!.tool_mode ?? false,
});
const handleConfirm = useCallback(() => {
addFlow({
flow: flowComponent,
override: true,
});
setSuccessData({ title: `${data.id} successfully overridden!` });
setShowOverrideModal(false);
}, [flowComponent, setSuccessData, setShowOverrideModal]);
const handleClose = useCallback(() => {
setShowOverrideModal(false);
}, []);
const handleCancel = useCallback(() => {
addFlow({
flow: flowComponent,
override: true,
});
setSuccessData({ title: "New component successfully saved!" });
setShowOverrideModal(false);
}, [flowComponent, setSuccessData, setShowOverrideModal]);
return (
<>
<div className="noflow nopan nodelete nodrag">
@ -754,29 +776,10 @@ export default function NodeToolbarComponent({
<ConfirmationModal
open={showOverrideModal}
title={`Replace`}
cancelText="Create New"
confirmationText="Replace"
size={"x-small"}
icon={"SaveAll"}
index={6}
onConfirm={() => {
addFlow({
flow: flowComponent,
override: true,
});
setSuccessData({ title: `${data.id} successfully overridden!` });
setShowOverrideModal(false);
}}
onClose={() => setShowOverrideModal(false)}
onCancel={() => {
addFlow({
flow: flowComponent,
override: true,
});
setSuccessData({ title: "New component successfully saved!" });
setShowOverrideModal(false);
}}
title="Replace"
onConfirm={handleConfirm}
onClose={handleClose}
onCancel={handleCancel}
>
<ConfirmationModal.Content>
<span>

View file

@ -11,7 +11,7 @@ async function zoomOut(page: Page, times: number = 4) {
test(
"should create a flow with decision",
{ tag: ["@release", "@components"] },
{ tag: ["@release", "@components", "@workflow"] },
async ({ page }) => {
test.skip(