diff --git a/examples/vite-app/src/examples/Undirectional/index.tsx b/examples/vite-app/src/examples/Undirectional/index.tsx
index 4ea676a5..4f25f4a5 100644
--- a/examples/vite-app/src/examples/Undirectional/index.tsx
+++ b/examples/vite-app/src/examples/Undirectional/index.tsx
@@ -1,4 +1,4 @@
-import React, { MouseEvent, useCallback } from 'react';
+import { MouseEvent, useCallback } from 'react';
import ReactFlow, {
useReactFlow,
NodeTypes,
@@ -184,6 +184,7 @@ const UpdateNodeInternalsFlow = () => {
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
+
const { project } = useReactFlow();
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
setEdges((els) => updateEdge(oldEdge, newConnection, els));
diff --git a/packages/core/src/components/ConnectionLine/index.tsx b/packages/core/src/components/ConnectionLine/index.tsx
index 3693f769..fae0c3b1 100644
--- a/packages/core/src/components/ConnectionLine/index.tsx
+++ b/packages/core/src/components/ConnectionLine/index.tsx
@@ -6,16 +6,15 @@ import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import { internalsSymbol } from '../../utils';
-import type { ConnectionLineComponent, HandleType, ReactFlowStore } from '../../types';
+import type { ConnectionLineComponent, HandleType, ReactFlowState, ReactFlowStore } from '../../types';
import { Position, ConnectionLineType, ConnectionMode } from '../../types';
type ConnectionLineProps = {
- connectionNodeId: string;
- connectionHandleType: HandleType;
- connectionLineType: ConnectionLineType;
- isConnectable: boolean;
- connectionLineStyle?: CSSProperties;
- CustomConnectionLineComponent?: ConnectionLineComponent;
+ nodeId: string;
+ handleType: HandleType;
+ type: ConnectionLineType;
+ style?: CSSProperties;
+ CustomComponent?: ConnectionLineComponent;
};
const oppositePosition = {
@@ -26,69 +25,62 @@ const oppositePosition = {
};
const ConnectionLine = ({
- connectionNodeId,
- connectionHandleType,
- connectionLineStyle,
- connectionLineType = ConnectionLineType.Bezier,
- isConnectable,
- CustomConnectionLineComponent,
+ nodeId,
+ handleType,
+ style,
+ type = ConnectionLineType.Bezier,
+ CustomComponent,
}: ConnectionLineProps) => {
const { fromNode, handleId, toX, toY, connectionMode } = useStore(
useCallback(
(s: ReactFlowStore) => ({
- fromNode: s.nodeInternals.get(connectionNodeId),
+ fromNode: s.nodeInternals.get(nodeId),
handleId: s.connectionHandleId,
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
connectionMode: s.connectionMode,
}),
- [connectionNodeId]
+ [nodeId]
),
shallow
);
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
- let handleBounds = fromHandleBounds?.[connectionHandleType];
+ let handleBounds = fromHandleBounds?.[handleType];
if (connectionMode === ConnectionMode.Loose) {
- handleBounds = handleBounds
- ? handleBounds
- : fromHandleBounds?.[connectionHandleType === 'source' ? 'target' : 'source'];
+ handleBounds = handleBounds ? handleBounds : fromHandleBounds?.[handleType === 'source' ? 'target' : 'source'];
}
- if (!fromNode || !isConnectable || !handleBounds) {
+ if (!fromNode || !handleBounds) {
return null;
}
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
- const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode?.width ?? 0) / 2;
- const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0;
- const fromX = (fromNode?.positionAbsolute?.x || 0) + fromHandleX;
- const fromY = (fromNode?.positionAbsolute?.y || 0) + fromHandleY;
-
+ const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.width ?? 0) / 2;
+ const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.height ?? 0;
+ const fromX = (fromNode.positionAbsolute?.x ?? 0) + fromHandleX;
+ const fromY = (fromNode.positionAbsolute?.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position;
+ const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
- if (!fromPosition) {
+ if (!fromPosition || !toPosition) {
return null;
}
- const toPosition: Position = oppositePosition[fromPosition];
-
- if (CustomConnectionLineComponent) {
+ if (CustomComponent) {
return (
-
-
-
+
);
}
@@ -103,29 +95,62 @@ const ConnectionLine = ({
targetPosition: toPosition,
};
- if (connectionLineType === ConnectionLineType.Bezier) {
+ if (type === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
[dAttr] = getBezierPath(pathParams);
- } else if (connectionLineType === ConnectionLineType.Step) {
+ } else if (type === ConnectionLineType.Step) {
[dAttr] = getSmoothStepPath({
...pathParams,
borderRadius: 0,
});
- } else if (connectionLineType === ConnectionLineType.SmoothStep) {
+ } else if (type === ConnectionLineType.SmoothStep) {
[dAttr] = getSmoothStepPath(pathParams);
- } else if (connectionLineType === ConnectionLineType.SimpleBezier) {
+ } else if (type === ConnectionLineType.SimpleBezier) {
[dAttr] = getSimpleBezierPath(pathParams);
} else {
dAttr = `M${fromX},${fromY} ${toX},${toY}`;
}
- return (
-
-
-
- );
+ return
;
};
ConnectionLine.displayName = 'ConnectionLine';
-export default ConnectionLine;
+type ConnectionLineWrapperProps = {
+ type: ConnectionLineType;
+ component?: ConnectionLineComponent;
+ containerStyle?: CSSProperties;
+ style?: CSSProperties;
+};
+
+const selector = (s: ReactFlowState) => ({
+ nodeId: s.connectionNodeId,
+ handleType: s.connectionHandleType,
+ nodesConnectable: s.nodesConnectable,
+ width: s.width,
+ height: s.height,
+});
+
+function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) {
+ const { nodeId, handleType, nodesConnectable, width, height } = useStore(selector, shallow);
+ const isValid = !!(nodeId && handleType && width && nodesConnectable);
+
+ if (!isValid) {
+ return null;
+ }
+
+ return (
+
+ );
+}
+
+export default ConnectionLineWrapper;
diff --git a/packages/core/src/components/Edges/wrapEdge.tsx b/packages/core/src/components/Edges/wrapEdge.tsx
index 36c49615..9d10f944 100644
--- a/packages/core/src/components/Edges/wrapEdge.tsx
+++ b/packages/core/src/components/Edges/wrapEdge.tsx
@@ -9,7 +9,7 @@ import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph';
import { getMouseHandler } from './utils';
import { elementSelectionKeys } from '../../utils';
-import type { EdgeProps, WrapEdgeProps, Connection } from '../../types';
+import type { EdgeProps, WrapEdgeProps, Connection, HandleType } from '../../types';
export default (EdgeComponent: ComponentType
) => {
const EdgeWrapper = ({
@@ -115,7 +115,7 @@ export default (EdgeComponent: ComponentType) => {
getState: store.getState,
setState: store.setState,
isValidConnection,
- elementEdgeUpdaterType: handleType,
+ edgeUpdaterType: handleType,
onEdgeUpdateEnd: _onEdgeUpdateEnd,
});
};
diff --git a/packages/core/src/components/Handle/index.tsx b/packages/core/src/components/Handle/index.tsx
index aa887d2f..59d2bd7f 100644
--- a/packages/core/src/components/Handle/index.tsx
+++ b/packages/core/src/components/Handle/index.tsx
@@ -5,11 +5,12 @@ import { shallow } from 'zustand/shallow';
import { useStore, useStoreApi } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext';
import { handlePointerDown } from './handler';
-import { devWarn, getHostForElement, isMouseEvent } from '../../utils';
+import { getHostForElement, isMouseEvent } from '../../utils';
import { addEdge } from '../../utils/graph';
import { Position } from '../../types';
import type { HandleProps, Connection, ReactFlowState } from '../../types';
import { isValidHandle } from './utils';
+import { errorMessages } from '../../contants';
const alwaysValid = () => true;
@@ -42,7 +43,8 @@ const Handle = forwardRef(
const nodeId = useNodeId();
if (!nodeId) {
- devWarn('Handle: No node id found. Make sure to only use a Handle inside a custom Node.');
+ store.getState().onError?.('010', errorMessages['010']());
+
return null;
}
diff --git a/packages/core/src/components/Handle/utils.ts b/packages/core/src/components/Handle/utils.ts
index 74c0ef5a..a049a2cc 100644
--- a/packages/core/src/components/Handle/utils.ts
+++ b/packages/core/src/components/Handle/utils.ts
@@ -98,7 +98,7 @@ export function isValidHandle(
const isValid =
connectionMode === ConnectionMode.Strict
? (isTarget && handleIsSource) || (!isTarget && handleIsTarget)
- : handleNodeId !== handle.nodeId || handleId !== handle.id;
+ : handleNodeId !== fromNodeId || handleId !== fromHandleId;
if (isValid) {
result.isValid = isValidConnection(connection);
diff --git a/packages/core/src/components/StoreUpdater/index.tsx b/packages/core/src/components/StoreUpdater/index.tsx
index 55af8fb4..c5645944 100644
--- a/packages/core/src/components/StoreUpdater/index.tsx
+++ b/packages/core/src/components/StoreUpdater/index.tsx
@@ -47,6 +47,7 @@ type StoreUpdaterProps = Pick<
| 'elevateNodesOnSelect'
| 'autoPanOnConnect'
| 'autoPanOnNodeDrag'
+ | 'onError'
> & { rfId: string };
const selector = (s: ReactFlowState) => ({
@@ -123,6 +124,7 @@ const StoreUpdater = ({
rfId,
autoPanOnConnect,
autoPanOnNodeDrag,
+ onError,
}: StoreUpdaterProps) => {
const {
setNodes,
@@ -178,6 +180,7 @@ const StoreUpdater = ({
useDirectStoreUpdater('rfId', rfId, store.setState);
useDirectStoreUpdater('autoPanOnConnect', autoPanOnConnect, store.setState);
useDirectStoreUpdater('autoPanOnNodeDrag', autoPanOnNodeDrag, store.setState);
+ useDirectStoreUpdater('onError', onError, store.setState);
useStoreUpdater(nodes, setNodes);
useStoreUpdater(edges, setEdges);
diff --git a/packages/core/src/container/EdgeRenderer/MarkerSymbols.tsx b/packages/core/src/container/EdgeRenderer/MarkerSymbols.tsx
index fbdd4972..7429f635 100644
--- a/packages/core/src/container/EdgeRenderer/MarkerSymbols.tsx
+++ b/packages/core/src/container/EdgeRenderer/MarkerSymbols.tsx
@@ -1,8 +1,9 @@
import { useMemo } from 'react';
-import { devWarn } from '../../utils';
import { MarkerType } from '../../types';
import type { EdgeMarker } from '../../types';
+import { useStoreApi } from '../../hooks/useStore';
+import { errorMessages } from '../../contants';
type SymbolProps = Omit;
@@ -38,11 +39,12 @@ export const MarkerSymbols = {
};
export function useMarkerSymbol(type: MarkerType) {
+ const store = useStoreApi();
const symbol = useMemo(() => {
const symbolExists = Object.prototype.hasOwnProperty.call(MarkerSymbols, type);
if (!symbolExists) {
- devWarn(`Marker type "${type}" doesn't exist. Help: https://reactflow.dev/error#900`);
+ store.getState().onError?.('009', errorMessages['009'](type));
return null;
}
diff --git a/packages/core/src/container/EdgeRenderer/index.tsx b/packages/core/src/container/EdgeRenderer/index.tsx
index c687a8d8..ddc4aa4d 100644
--- a/packages/core/src/container/EdgeRenderer/index.tsx
+++ b/packages/core/src/container/EdgeRenderer/index.tsx
@@ -1,27 +1,20 @@
-import { memo } from 'react';
+import { memo, ReactNode } from 'react';
import { shallow } from 'zustand/shallow';
import cc from 'classcat';
import { useStore } from '../../hooks/useStore';
import useVisibleEdges from '../../hooks/useVisibleEdges';
-import ConnectionLine from '../../components/ConnectionLine/index';
import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getHandle, getNodeData } from './utils';
import { GraphViewProps } from '../GraphView';
-import { devWarn } from '../../utils';
import { ConnectionMode, Position } from '../../types';
import type { Edge, ReactFlowState } from '../../types';
+import { errorMessages } from '../../contants';
type EdgeRendererProps = Pick<
GraphViewProps,
| 'edgeTypes'
- | 'connectionLineType'
- | 'connectionLineType'
- | 'connectionLineStyle'
- | 'connectionLineComponent'
- | 'connectionLineContainerStyle'
- | 'connectionLineContainerStyle'
| 'onEdgeClick'
| 'onEdgeDoubleClick'
| 'defaultMarkerColor'
@@ -40,11 +33,10 @@ type EdgeRendererProps = Pick<
| 'disableKeyboardA11y'
> & {
elevateEdgesOnSelect: boolean;
+ children: ReactNode;
};
const selector = (s: ReactFlowState) => ({
- connectionNodeId: s.connectionNodeId,
- connectionHandleType: s.connectionHandleType,
nodesConnectable: s.nodesConnectable,
edgesFocusable: s.edgesFocusable,
elementsSelectable: s.elementsSelectable,
@@ -52,35 +44,38 @@ const selector = (s: ReactFlowState) => ({
height: s.height,
connectionMode: s.connectionMode,
nodeInternals: s.nodeInternals,
+ onError: s.onError,
});
-const EdgeRenderer = (props: EdgeRendererProps) => {
- const {
- connectionNodeId,
- connectionHandleType,
- nodesConnectable,
- edgesFocusable,
- elementsSelectable,
- width,
- height,
- connectionMode,
- nodeInternals,
- } = useStore(selector, shallow);
- const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals, props.elevateEdgesOnSelect);
+const EdgeRenderer = ({
+ defaultMarkerColor,
+ onlyRenderVisibleElements,
+ elevateEdgesOnSelect,
+ rfId,
+ edgeTypes,
+ noPanClassName,
+ onEdgeUpdate,
+ onEdgeContextMenu,
+ onEdgeMouseEnter,
+ onEdgeMouseMove,
+ onEdgeMouseLeave,
+ onEdgeClick,
+ edgeUpdaterRadius,
+ onEdgeDoubleClick,
+ onEdgeUpdateStart,
+ onEdgeUpdateEnd,
+ children,
+}: EdgeRendererProps) => {
+ const { edgesFocusable, elementsSelectable, width, height, connectionMode, nodeInternals, onError } = useStore(
+ selector,
+ shallow
+ );
+ const edgeTree = useVisibleEdges(onlyRenderVisibleElements, nodeInternals, elevateEdgesOnSelect);
if (!width) {
return null;
}
- const {
- connectionLineType,
- defaultMarkerColor,
- connectionLineStyle,
- connectionLineComponent,
- connectionLineContainerStyle,
- } = props;
- const renderConnectionLine = connectionNodeId && connectionHandleType;
-
return (
<>
{edgeTree.map(({ level, edges, isMaxLevel }) => (
@@ -91,11 +86,11 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
height={height}
className="react-flow__edges react-flow__container"
>
- {isMaxLevel && }
+ {isMaxLevel && }
{edges.map((edge: Edge) => {
- const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source)!);
- const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target)!);
+ const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source));
+ const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target));
if (!sourceIsValid || !targetIsValid) {
return null;
@@ -103,32 +98,25 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
let edgeType = edge.type || 'default';
- if (!props.edgeTypes[edgeType]) {
- devWarn(
- `Edge type "${edgeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300`
- );
-
+ if (!edgeTypes[edgeType]) {
+ onError?.('011', errorMessages['011'](edgeType));
edgeType = 'default';
}
- const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default;
- // when connection type is loose we can define all handles as sources
+ const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default;
+ // when connection type is loose we can define all handles as sources and connect source -> source
const targetNodeHandles =
connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []);
- const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle || null);
- const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle || null);
+ const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle);
+ const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
if (!sourceHandle || !targetHandle) {
- devWarn(
- `Couldn't create edge for ${!sourceHandle ? 'source' : 'target'} handle id: ${
- !sourceHandle ? edge.sourceHandle : edge.targetHandle
- }; edge id: ${edge.id}. Help: https://reactflow.dev/error#800`
- );
+ onError?.('008', errorMessages['008'](sourceHandle, edge));
return null;
}
@@ -146,7 +134,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
{
sourcePosition={sourcePosition}
targetPosition={targetPosition}
elementsSelectable={elementsSelectable}
- onEdgeUpdate={props.onEdgeUpdate}
- onContextMenu={props.onEdgeContextMenu}
- onMouseEnter={props.onEdgeMouseEnter}
- onMouseMove={props.onEdgeMouseMove}
- onMouseLeave={props.onEdgeMouseLeave}
- onClick={props.onEdgeClick}
- edgeUpdaterRadius={props.edgeUpdaterRadius}
- onEdgeDoubleClick={props.onEdgeDoubleClick}
- onEdgeUpdateStart={props.onEdgeUpdateStart}
- onEdgeUpdateEnd={props.onEdgeUpdateEnd}
- rfId={props.rfId}
+ onEdgeUpdate={onEdgeUpdate}
+ onContextMenu={onEdgeContextMenu}
+ onMouseEnter={onEdgeMouseEnter}
+ onMouseMove={onEdgeMouseMove}
+ onMouseLeave={onEdgeMouseLeave}
+ onClick={onEdgeClick}
+ edgeUpdaterRadius={edgeUpdaterRadius}
+ onEdgeDoubleClick={onEdgeDoubleClick}
+ onEdgeUpdateStart={onEdgeUpdateStart}
+ onEdgeUpdateEnd={onEdgeUpdateEnd}
+ rfId={rfId}
ariaLabel={edge.ariaLabel}
isFocusable={isFocusable}
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
@@ -193,23 +181,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
))}
- {renderConnectionLine && (
-
- )}
+ {children}
>
);
};
diff --git a/packages/core/src/container/EdgeRenderer/utils.ts b/packages/core/src/container/EdgeRenderer/utils.ts
index eee01706..edd74543 100644
--- a/packages/core/src/container/EdgeRenderer/utils.ts
+++ b/packages/core/src/container/EdgeRenderer/utils.ts
@@ -72,21 +72,18 @@ export function getHandlePosition(position: Position, nodeRect: Rect, handle: Ha
}
}
-export function getHandle(bounds: HandleElement[], handleId: string | null): HandleElement | null {
+export function getHandle(bounds: HandleElement[], handleId?: string | null): HandleElement | null {
if (!bounds) {
return null;
}
- // there is no handleId when there are no multiple handles/ handles with ids
- // so we just pick the first one
- let handle: HandleElement | null = null;
- if (bounds.length === 1 || !handleId) {
- handle = bounds[0];
- } else if (handleId) {
- handle = bounds.find((d) => d.id === handleId)!;
+ if (handleId) {
+ return bounds.find((d) => d.id === handleId)!;
+ } else if (bounds.length === 1) {
+ return bounds[0];
}
- return typeof handle === 'undefined' ? null : handle;
+ return null;
}
interface EdgePositions {
@@ -167,16 +164,15 @@ export function isEdgeVisible({
return overlappingArea > 0;
}
-export function getNodeData(node: Node): [Rect, NodeHandleBounds | null, boolean] {
+export function getNodeData(node?: Node): [Rect, NodeHandleBounds | null, boolean] {
const handleBounds = node?.[internalsSymbol]?.handleBounds || null;
- const isInvalid =
- !node ||
- !handleBounds ||
- !node.width ||
- !node.height ||
- typeof node.positionAbsolute?.x === 'undefined' ||
- typeof node.positionAbsolute?.y === 'undefined';
+ const isValid =
+ handleBounds &&
+ node?.width &&
+ node?.height &&
+ typeof node?.positionAbsolute?.x !== 'undefined' &&
+ typeof node?.positionAbsolute?.y !== 'undefined';
return [
{
@@ -186,6 +182,6 @@ export function getNodeData(node: Node): [Rect, NodeHandleBounds | null, boolean
height: node?.height || 0,
},
handleBounds,
- !isInvalid,
+ !!isValid,
];
}
diff --git a/packages/core/src/container/GraphView/index.tsx b/packages/core/src/container/GraphView/index.tsx
index 425d9180..3de2550c 100644
--- a/packages/core/src/container/GraphView/index.tsx
+++ b/packages/core/src/container/GraphView/index.tsx
@@ -6,6 +6,7 @@ import EdgeRenderer from '../EdgeRenderer';
import ViewportWrapper from '../Viewport';
import useOnInitHandler from '../../hooks/useOnInitHandler';
import type { EdgeTypesWrapped, NodeTypesWrapped, ReactFlowProps } from '../../types';
+import ConnectionLine from '../../components/ConnectionLine';
export type GraphViewProps = Omit &
Required<
@@ -149,10 +150,6 @@ const GraphView = ({
edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick}
- connectionLineType={connectionLineType}
- connectionLineStyle={connectionLineStyle}
- connectionLineComponent={connectionLineComponent}
- connectionLineContainerStyle={connectionLineContainerStyle}
onEdgeUpdate={onEdgeUpdate}
onlyRenderVisibleElements={onlyRenderVisibleElements}
onEdgeContextMenu={onEdgeContextMenu}
@@ -167,7 +164,14 @@ const GraphView = ({
elevateEdgesOnSelect={!!elevateEdgesOnSelect}
disableKeyboardA11y={disableKeyboardA11y}
rfId={rfId}
- />
+ >
+
+
({
nodesFocusable: s.nodesFocusable,
elementsSelectable: s.elementsSelectable,
updateNodeDimensions: s.updateNodeDimensions,
+ onError: s.onError,
});
const NodeRenderer = (props: NodeRendererProps) => {
- const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions } = useStore(
- selector,
- shallow
- );
+ const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions, onError } =
+ useStore(selector, shallow);
const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
const resizeObserverRef = useRef();
@@ -78,9 +78,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
let nodeType = node.type || 'default';
if (!props.nodeTypes[nodeType]) {
- devWarn(
- `Node type "${nodeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300`
- );
+ onError?.('003', errorMessages['003'](nodeType));
nodeType = 'default';
}
diff --git a/packages/core/src/container/NodeRenderer/utils.ts b/packages/core/src/container/NodeRenderer/utils.ts
index ebac7413..6abc695d 100644
--- a/packages/core/src/container/NodeRenderer/utils.ts
+++ b/packages/core/src/container/NodeRenderer/utils.ts
@@ -5,7 +5,6 @@ import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode';
import GroupNode from '../../components/Nodes/GroupNode';
import wrapNode from '../../components/Nodes/wrapNode';
-import { devWarn } from '../../utils';
import type { NodeTypes, NodeProps, NodeTypesWrapped, NodeOrigin, XYPosition } from '../../types';
export type CreateNodeTypes = (nodeTypes: NodeTypes) => NodeTypesWrapped;
@@ -51,7 +50,6 @@ export const getPositionWithOrigin = ({
}
if (origin[0] < 0 || origin[1] < 0 || origin[0] > 1 || origin[1] > 1) {
- devWarn('nodeOrigin must be between 0 and 1');
return { x, y };
}
diff --git a/packages/core/src/container/ReactFlow/index.tsx b/packages/core/src/container/ReactFlow/index.tsx
index 67964f0f..120a5efa 100644
--- a/packages/core/src/container/ReactFlow/index.tsx
+++ b/packages/core/src/container/ReactFlow/index.tsx
@@ -163,6 +163,7 @@ const ReactFlow = forwardRef(
autoPanOnConnect = true,
autoPanOnNodeDrag = true,
connectionRadius = 20,
+ onError,
style,
id,
...rest
@@ -292,6 +293,7 @@ const ReactFlow = forwardRef(
rfId={rfId}
autoPanOnConnect={autoPanOnConnect}
autoPanOnNodeDrag={autoPanOnNodeDrag}
+ onError={onError}
/>
{children}
diff --git a/packages/core/src/container/ReactFlow/utils.ts b/packages/core/src/container/ReactFlow/utils.ts
index 2f8a9239..cc5ab36d 100644
--- a/packages/core/src/container/ReactFlow/utils.ts
+++ b/packages/core/src/container/ReactFlow/utils.ts
@@ -5,6 +5,7 @@ import { devWarn } from '../../utils';
import { CreateEdgeTypes } from '../EdgeRenderer/utils';
import { CreateNodeTypes } from '../NodeRenderer/utils';
import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
+import { errorMessages } from '../../contants';
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped;
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped;
@@ -16,9 +17,7 @@ export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any
if (process.env.NODE_ENV === 'development') {
const typeKeys = Object.keys(nodeOrEdgeTypes);
if (shallow(typesKeysRef.current, typeKeys)) {
- devWarn(
- "It looks like you have created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them. Help: https://reactflow.dev/error#200"
- );
+ devWarn('002', errorMessages['002']());
}
typesKeysRef.current = typeKeys;
diff --git a/packages/core/src/contants.ts b/packages/core/src/contants.ts
new file mode 100644
index 00000000..815ac6f0
--- /dev/null
+++ b/packages/core/src/contants.ts
@@ -0,0 +1,20 @@
+import { Edge, HandleElement } from './types';
+
+export const errorMessages = {
+ '001': () =>
+ '[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#001',
+ '002': () =>
+ "It looks like you've created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them.",
+ '003': (nodeType: string) => `Node type "${nodeType}" not found. Using fallback type "default".`,
+ '004': () => 'The React Flow parent container needs a width and a height to render the graph.',
+ '005': () => 'Only child nodes can use a parent extent.',
+ '006': () => "Can't create edge. An edge needs a source and a target.",
+ '007': (id: string) => `The old edge with id=${id} does not exist.`,
+ '009': (type: string) => `Marker type "${type}" doesn't exist.`,
+ '008': (sourceHandle: HandleElement | null, edge: Edge) =>
+ `Couldn't create edge for ${!sourceHandle ? 'source' : 'target'} handle id: "${
+ !sourceHandle ? edge.sourceHandle : edge.targetHandle
+ }", edge id: ${edge.id}.`,
+ '010': () => 'Handle: No node id found. Make sure to only use a Handle inside a custom Node.',
+ '011': (edgeType: string) => `Edge type "${edgeType}" not found. Using fallback type "default".`,
+};
diff --git a/packages/core/src/hooks/useDrag/index.ts b/packages/core/src/hooks/useDrag/index.ts
index 868b6387..c6f3d269 100644
--- a/packages/core/src/hooks/useDrag/index.ts
+++ b/packages/core/src/hooks/useDrag/index.ts
@@ -61,6 +61,7 @@ function useDrag({
snapGrid,
snapToGrid,
nodeOrigin,
+ onError,
} = store.getState();
lastPos.current = { x, y };
@@ -75,7 +76,7 @@ function useDrag({
nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]);
}
- const updatedPos = calcNextPosition(n, nextPosition, nodeInternals, nodeExtent, nodeOrigin);
+ const updatedPos = calcNextPosition(n, nextPosition, nodeInternals, nodeExtent, nodeOrigin, onError);
// we want to make sure that we only fire a change event when there is a changes
hasChange = hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y;
diff --git a/packages/core/src/hooks/useDrag/utils.ts b/packages/core/src/hooks/useDrag/utils.ts
index 74484baf..ed40053b 100644
--- a/packages/core/src/hooks/useDrag/utils.ts
+++ b/packages/core/src/hooks/useDrag/utils.ts
@@ -1,8 +1,9 @@
import type { RefObject } from 'react';
-import { clampPosition, devWarn, isNumeric } from '../../utils';
-import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, NodeOrigin, XYPosition } from '../../types';
+import { clampPosition, isNumeric } from '../../utils';
+import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, NodeOrigin, OnError, XYPosition } from '../../types';
import { getNodePositionWithOrigin } from '../../utils/graph';
+import { errorMessages } from '../../contants';
export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean {
if (!node.parentNode) {
@@ -62,7 +63,8 @@ export function calcNextPosition(
nextPosition: XYPosition,
nodeInternals: NodeInternals,
nodeExtent?: CoordinateExtent,
- nodeOrigin: NodeOrigin = [0, 0]
+ nodeOrigin: NodeOrigin = [0, 0],
+ onError?: OnError
): { position: XYPosition; positionAbsolute: XYPosition } {
let currentExtent = node.extent || nodeExtent;
@@ -81,7 +83,7 @@ export function calcNextPosition(
]
: currentExtent;
} else {
- devWarn('Only child nodes can use a parent extent. Help: https://reactflow.dev/error#500');
+ onError?.('005', errorMessages['005']());
currentExtent = nodeExtent;
}
diff --git a/packages/core/src/hooks/useResizeHandler.ts b/packages/core/src/hooks/useResizeHandler.ts
index d2ca3e63..a6d6d921 100644
--- a/packages/core/src/hooks/useResizeHandler.ts
+++ b/packages/core/src/hooks/useResizeHandler.ts
@@ -2,7 +2,8 @@ import { useEffect } from 'react';
import type { MutableRefObject } from 'react';
import { useStoreApi } from '../hooks/useStore';
-import { devWarn, getDimensions } from '../utils';
+import { getDimensions } from '../utils';
+import { errorMessages } from '../contants';
function useResizeHandler(rendererNode: MutableRefObject): void {
const store = useStoreApi();
@@ -18,9 +19,7 @@ function useResizeHandler(rendererNode: MutableRefObject)
const size = getDimensions(rendererNode.current);
if (size.height === 0 || size.width === 0) {
- devWarn(
- 'The React Flow parent container needs a width and a height to render the graph. Help: https://reactflow.dev/error#400'
- );
+ store.getState().onError?.('004', errorMessages['004']());
}
store.setState({ width: size.width || 500, height: size.height || 500 });
diff --git a/packages/core/src/hooks/useStore.ts b/packages/core/src/hooks/useStore.ts
index bc0e605e..3983579d 100644
--- a/packages/core/src/hooks/useStore.ts
+++ b/packages/core/src/hooks/useStore.ts
@@ -3,10 +3,10 @@ import { useStore as useZustandStore } from 'zustand';
import type { StoreApi } from 'zustand';
import StoreContext from '../contexts/RFStoreContext';
+import { errorMessages } from '../contants';
import type { ReactFlowState } from '../types';
-const errorMessage =
- '[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#100';
+const zustandErrorMessage = errorMessages['001']();
type ExtractState = StoreApi extends { getState: () => infer T } ? T : never;
@@ -17,7 +17,7 @@ function useStore(
const store = useContext(StoreContext);
if (store === null) {
- throw new Error(errorMessage);
+ throw new Error(zustandErrorMessage);
}
return useZustandStore(store, selector, equalityFn);
@@ -27,7 +27,7 @@ const useStoreApi = () => {
const store = useContext(StoreContext);
if (store === null) {
- throw new Error(errorMessage);
+ throw new Error(zustandErrorMessage);
}
return useMemo(
diff --git a/packages/core/src/hooks/useUpdateNodePositions.ts b/packages/core/src/hooks/useUpdateNodePositions.ts
index b20ea4d1..131f85cd 100644
--- a/packages/core/src/hooks/useUpdateNodePositions.ts
+++ b/packages/core/src/hooks/useUpdateNodePositions.ts
@@ -7,7 +7,8 @@ function useUpdateNodePositions() {
const store = useStoreApi();
const updatePositions = useCallback((params: { x: number; y: number; isShiftPressed: boolean }) => {
- const { nodeInternals, nodeExtent, updateNodePositions, getNodes, snapToGrid, snapGrid } = store.getState();
+ const { nodeInternals, nodeExtent, updateNodePositions, getNodes, snapToGrid, snapGrid, onError } =
+ store.getState();
const selectedNodes = getNodes().filter((n) => n.selected);
// by default a node moves 5px on each key press, or 20px if shift is pressed
// if snap grid is enabled, we use that for the velocity.
@@ -27,10 +28,17 @@ function useUpdateNodePositions() {
nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]);
}
- const updatedPos = calcNextPosition(n, nextPosition, nodeInternals, nodeExtent);
+ const { positionAbsolute, position } = calcNextPosition(
+ n,
+ nextPosition,
+ nodeInternals,
+ nodeExtent,
+ undefined,
+ onError
+ );
- n.position = updatedPos.position;
- n.positionAbsolute = updatedPos.positionAbsolute;
+ n.position = position;
+ n.positionAbsolute = positionAbsolute;
}
return n;
diff --git a/packages/core/src/store/initialState.ts b/packages/core/src/store/initialState.ts
index 6c475b5b..afccec2b 100644
--- a/packages/core/src/store/initialState.ts
+++ b/packages/core/src/store/initialState.ts
@@ -1,3 +1,4 @@
+import { devWarn } from '../utils';
import { ConnectionMode } from '../types';
import type { CoordinateExtent, ReactFlowStore } from '../types';
@@ -59,6 +60,7 @@ const initialState: ReactFlowStore = {
autoPanOnConnect: true,
autoPanOnNodeDrag: true,
connectionRadius: 20,
+ onError: devWarn,
};
export default initialState;
diff --git a/packages/core/src/styles/init.css b/packages/core/src/styles/init.css
index a621794f..9cecc294 100644
--- a/packages/core/src/styles/init.css
+++ b/packages/core/src/styles/init.css
@@ -98,7 +98,7 @@
.react-flow__connection {
pointer-events: none;
- &.animated {
+ .animated {
stroke-dasharray: 5;
animation: dashdraw 0.5s linear infinite;
}
diff --git a/packages/core/src/types/component-props.ts b/packages/core/src/types/component-props.ts
index bafb8d5e..1614805a 100644
--- a/packages/core/src/types/component-props.ts
+++ b/packages/core/src/types/component-props.ts
@@ -36,6 +36,7 @@ import type {
EdgeMouseHandler,
HandleType,
SelectionMode,
+ OnError,
} from '.';
export type ReactFlowProps = HTMLAttributes & {
@@ -142,6 +143,7 @@ export type ReactFlowProps = HTMLAttributes & {
autoPanOnNodeDrag?: boolean;
autoPanOnConnect?: boolean;
connectionRadius?: number;
+ onError?: OnError;
};
export type ReactFlowRefType = HTMLDivElement;
diff --git a/packages/core/src/types/general.ts b/packages/core/src/types/general.ts
index ba0de157..9dbd3dce 100644
--- a/packages/core/src/types/general.ts
+++ b/packages/core/src/types/general.ts
@@ -206,6 +206,7 @@ export type ReactFlowStore = {
onNodesDelete?: OnNodesDelete;
onEdgesDelete?: OnEdgesDelete;
+ onError?: OnError;
// event handlers
onViewportChangeStart?: OnViewportChange;
@@ -270,3 +271,5 @@ export type SelectionRect = Rect & {
startX: number;
startY: number;
};
+
+export type OnError = (id: string, message: string) => void;
diff --git a/packages/core/src/utils/graph.ts b/packages/core/src/utils/graph.ts
index 7e94f766..c7772a1c 100644
--- a/packages/core/src/utils/graph.ts
+++ b/packages/core/src/utils/graph.ts
@@ -13,6 +13,7 @@ import {
NodeInternals,
NodeOrigin,
} from '../types';
+import { errorMessages } from '../contants';
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
'id' in element && 'source' in element && 'target' in element;
@@ -70,7 +71,7 @@ const connectionExists = (edge: Edge, edges: Edge[]) => {
export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] => {
if (!edgeParams.source || !edgeParams.target) {
- devWarn("Can't create edge. An edge needs a source and a target. Help: https://reactflow.dev/error#600");
+ devWarn('006', errorMessages['006']());
return edges;
}
@@ -94,7 +95,7 @@ export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] =>
export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge[]): Edge[] => {
if (!newConnection.source || !newConnection.target) {
- devWarn("Can't create a new edge. An edge needs a source and a target. Help: https://reactflow.dev/error#600");
+ devWarn('006', errorMessages['006']());
return edges;
}
@@ -102,7 +103,7 @@ export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge
const foundEdge = edges.find((e) => e.id === oldEdge.id) as Edge;
if (!foundEdge) {
- devWarn(`The old edge with id=${oldEdge.id} does not exist. Help: https://reactflow.dev/error#700`);
+ devWarn('007', errorMessages['007'](oldEdge.id));
return edges;
}
diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts
index 5bfd52c2..f1021bfb 100644
--- a/packages/core/src/utils/index.ts
+++ b/packages/core/src/utils/index.ts
@@ -89,9 +89,9 @@ export const internalsSymbol = Symbol.for('internals');
// used for a11y key board controls for nodes and edges
export const elementSelectionKeys = ['Enter', ' ', 'Escape'];
-export const devWarn = (message: string) => {
+export const devWarn = (id: string, message: string) => {
if (process.env.NODE_ENV === 'development') {
- console.warn(`[React Flow]: ${message}`);
+ console.warn(`[React Flow]: ${message} Help: https://reactflow.dev/error#${id}`);
}
};
@@ -118,7 +118,7 @@ export function isInputDOMNode(event: KeyboardEvent | ReactKeyboardEvent): boole
export const isMouseEvent = (
event: MouseEvent | ReactMouseEvent | TouchEvent | ReactTouchEvent
-): event is MouseEvent | ReactMouseEvent => 'button' in event;
+): event is MouseEvent | ReactMouseEvent => 'clientX' in event;
export const getEventPosition = (
event: MouseEvent | ReactMouseEvent | TouchEvent | ReactTouchEvent,