diff --git a/packages/react/package.json b/packages/react/package.json index 1cdf428c..af284012 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -57,7 +57,7 @@ "build": "rollup --config node:@xyflow/rollup-config --environment NODE_ENV:production && npm run css", "css": "postcss src/styles/{base,style}.css --config ./../../tooling/postcss-config/ --dir dist ", "css-watch": "pnpm css --watch", - "lint": "eslint --ext .js,.jsx,.ts,.tsx src", + "lint": "eslint --ext .js,.jsx,.ts,.tsx src --fix", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/react/src/additional-components/Background/Background.tsx b/packages/react/src/additional-components/Background/Background.tsx index e4470f10..ee89624d 100644 --- a/packages/react/src/additional-components/Background/Background.tsx +++ b/packages/react/src/additional-components/Background/Background.tsx @@ -90,4 +90,57 @@ function BackgroundComponent({ BackgroundComponent.displayName = 'Background'; +/** + * The `` component makes it convenient to render different types of backgrounds common in node-based UIs. It comes with three variants: lines, dots and cross. + * + * @example + * + * A simple example of how to use the Background component. + * + * ```tsx + * import { useState } from 'react'; + * import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react'; + * + * export default function Flow() { + * return ( + * + * + * + * ); + * } + * ``` + * + * @example + * + * In this example you can see how to combine multiple backgrounds + * + * ```tsx + * import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react'; + * import '@xyflow/react/dist/style.css'; + * + * export default function Flow() { + * return ( + * + * + * + * + * ); + * } + * ``` + * + * @remarks + * + * When combining multiple components it’s important to give each of them a unique id prop! + * + */ export const Background = memo(BackgroundComponent); diff --git a/packages/react/src/additional-components/Background/types.ts b/packages/react/src/additional-components/Background/types.ts index 0a035aae..9d1cd67b 100644 --- a/packages/react/src/additional-components/Background/types.ts +++ b/packages/react/src/additional-components/Background/types.ts @@ -24,7 +24,8 @@ export type BackgroundProps = { offset?: number | [number, number]; /** Line width of the Line pattern */ lineWidth?: number; - /** Variant of the pattern + /** + * Variant of the pattern * @example BackgroundVariant.Lines, BackgroundVariant.Dots, BackgroundVariant.Cross * 'lines', 'dots', 'cross' */ diff --git a/packages/react/src/additional-components/Controls/types.ts b/packages/react/src/additional-components/Controls/types.ts index f6ea0f8b..c36275d9 100644 --- a/packages/react/src/additional-components/Controls/types.ts +++ b/packages/react/src/additional-components/Controls/types.ts @@ -20,7 +20,8 @@ export type ControlProps = { onFitView?: () => void; /** Callback when interactivity is toggled */ onInteractiveChange?: (interactiveStatus: boolean) => void; - /** Position of the controls on the pane + /** + * Position of the controls on the pane * @example PanelPosition.TopLeft, PanelPosition.TopRight, * PanelPosition.BottomLeft, PanelPosition.BottomRight */ diff --git a/packages/react/src/additional-components/MiniMap/MiniMap.tsx b/packages/react/src/additional-components/MiniMap/MiniMap.tsx index bd7d6634..2f749395 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx @@ -44,8 +44,10 @@ function MiniMapComponent({ nodeClassName = '', nodeBorderRadius = 5, nodeStrokeWidth, - // We need to rename the prop to be `CapitalCase` so that JSX will render it as - // a component properly. + /* + * We need to rename the prop to be `CapitalCase` so that JSX will render it as + * a component properly. + */ nodeComponent, bgColor, maskColor, @@ -111,16 +113,16 @@ function MiniMapComponent({ const onSvgClick = onClick ? (event: MouseEvent) => { - const [x, y] = minimapInstance.current?.pointer(event) || [0, 0]; - onClick(event, { x, y }); - } + const [x, y] = minimapInstance.current?.pointer(event) || [0, 0]; + onClick(event, { x, y }); + } : undefined; const onSvgNodeClick = onNodeClick ? useCallback((event: MouseEvent, nodeId: string) => { - const node = store.getState().nodeLookup.get(nodeId)!; - onNodeClick(event, node); - }, []) + const node = store.getState().nodeLookup.get(nodeId)!; + onNodeClick(event, node); + }, []) : undefined; return ( diff --git a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx index 87fd21e5..2c314289 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx @@ -21,8 +21,10 @@ function MiniMapNodes({ nodeClassName = '', nodeBorderRadius = 5, nodeStrokeWidth, - // We need to rename the prop to be `CapitalCase` so that JSX will render it as - // a component properly. + /* + * We need to rename the prop to be `CapitalCase` so that JSX will render it as + * a component properly. + */ nodeComponent: NodeComponent = MiniMapNode, onClick, }: MiniMapNodesProps) { @@ -36,11 +38,13 @@ function MiniMapNodes({ return ( <> {nodeIds.map((nodeId) => ( - // The split of responsibilities between MiniMapNodes and - // NodeComponentWrapper may appear weird. However, it’s designed to - // minimize the cost of updates when individual nodes change. - // - // For more details, see a similar commit in `NodeRenderer/index.tsx`. + /* + * The split of responsibilities between MiniMapNodes and + * NodeComponentWrapper may appear weird. However, it’s designed to + * minimize the cost of updates when individual nodes change. + * + * For more details, see a similar commit in `NodeRenderer/index.tsx`. + */ key={nodeId} id={nodeId} diff --git a/packages/react/src/additional-components/MiniMap/types.ts b/packages/react/src/additional-components/MiniMap/types.ts index 4bfd1a51..c6d11ba8 100644 --- a/packages/react/src/additional-components/MiniMap/types.ts +++ b/packages/react/src/additional-components/MiniMap/types.ts @@ -27,7 +27,8 @@ export type MiniMapProps = Omit & { - /** Position of the control + /** + * Position of the control * @example ControlPosition.TopLeft, ControlPosition.TopRight, * ControlPosition.BottomLeft, ControlPosition.BottomRight */ position?: ControlPosition; - /** Variant of the control + /** + * Variant of the control * @example ResizeControlVariant.Handle, ResizeControlVariant.Line */ variant?: ResizeControlVariant; diff --git a/packages/react/src/additional-components/NodeToolbar/types.ts b/packages/react/src/additional-components/NodeToolbar/types.ts index 4de679bd..00bec37c 100644 --- a/packages/react/src/additional-components/NodeToolbar/types.ts +++ b/packages/react/src/additional-components/NodeToolbar/types.ts @@ -6,14 +6,16 @@ export type NodeToolbarProps = HTMLAttributes & { nodeId?: string | string[]; /** If true, node toolbar is visible even if node is not selected */ isVisible?: boolean; - /** Position of the toolbar relative to the node + /** + * Position of the toolbar relative to the node * @example Position.TopLeft, Position.TopRight, * Position.BottomLeft, Position.BottomRight */ position?: Position; /** Offset the toolbar from the node */ offset?: number; - /** Align the toolbar relative to the node + /** + * Align the toolbar relative to the node * @example Align.Start, Align.Center, Align.End */ align?: Align; diff --git a/packages/react/src/components/BatchProvider/index.tsx b/packages/react/src/components/BatchProvider/index.tsx index 1dbab718..5a5d1ed3 100644 --- a/packages/react/src/components/BatchProvider/index.tsx +++ b/packages/react/src/components/BatchProvider/index.tsx @@ -30,9 +30,11 @@ export function BatchProvider[]) => { const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState(); - // This is essentially an `Array.reduce` in imperative clothing. Processing - // this queue is a relatively hot path so we'd like to avoid the overhead of - // array methods where we can. + /* + * This is essentially an `Array.reduce` in imperative clothing. Processing + * this queue is a relatively hot path so we'd like to avoid the overhead of + * array methods where we can. + */ let next = nodes as NodeType[]; for (const payload of queueItems) { next = typeof payload === 'function' ? payload(next) : payload; diff --git a/packages/react/src/components/BatchProvider/useQueue.ts b/packages/react/src/components/BatchProvider/useQueue.ts index c33208d7..b1f9f793 100644 --- a/packages/react/src/components/BatchProvider/useQueue.ts +++ b/packages/react/src/components/BatchProvider/useQueue.ts @@ -12,21 +12,27 @@ import { Queue, QueueItem } from './types'; * @returns a Queue object */ export function useQueue(runQueue: (items: QueueItem[]) => void) { - // Because we're using a ref above, we need some way to let React know when to - // actually process the queue. We increment this number any time we mutate the - // queue, creating a new state to trigger the layout effect below. - // Using a boolean dirty flag here instead would lead to issues related to - // automatic batching. (https://github.com/xyflow/xyflow/issues/4779) + /* + * Because we're using a ref above, we need some way to let React know when to + * actually process the queue. We increment this number any time we mutate the + * queue, creating a new state to trigger the layout effect below. + * Using a boolean dirty flag here instead would lead to issues related to + * automatic batching. (https://github.com/xyflow/xyflow/issues/4779) + */ const [serial, setSerial] = useState(BigInt(0)); - // A reference of all the batched updates to process before the next render. We - // want a reference here so multiple synchronous calls to `setNodes` etc can be - // batched together. + /* + * A reference of all the batched updates to process before the next render. We + * want a reference here so multiple synchronous calls to `setNodes` etc can be + * batched together. + */ const [queue] = useState(() => createQueue(() => setSerial(n => n + BigInt(1)))); - // Layout effects are guaranteed to run before the next render which means we - // shouldn't run into any issues with stale state or weird issues that come from - // rendering things one frame later than expected (we used to use `setTimeout`). + /* + * Layout effects are guaranteed to run before the next render which means we + * shouldn't run into any issues with stale state or weird issues that come from + * rendering things one frame later than expected (we used to use `setTimeout`). + */ useIsomorphicLayoutEffect(() => { const queueItems = queue.get(); diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index f50ed4b9..2d3754fd 100644 --- a/packages/react/src/components/EdgeWrapper/index.tsx +++ b/packages/react/src/components/EdgeWrapper/index.tsx @@ -136,28 +136,28 @@ export function EdgeWrapper({ const onEdgeDoubleClick = onDoubleClick ? (event: React.MouseEvent) => { - onDoubleClick(event, { ...edge }); - } + onDoubleClick(event, { ...edge }); + } : undefined; const onEdgeContextMenu = onContextMenu ? (event: React.MouseEvent) => { - onContextMenu(event, { ...edge }); - } + onContextMenu(event, { ...edge }); + } : undefined; const onEdgeMouseEnter = onMouseEnter ? (event: React.MouseEvent) => { - onMouseEnter(event, { ...edge }); - } + onMouseEnter(event, { ...edge }); + } : undefined; const onEdgeMouseMove = onMouseMove ? (event: React.MouseEvent) => { - onMouseMove(event, { ...edge }); - } + onMouseMove(event, { ...edge }); + } : undefined; const onEdgeMouseLeave = onMouseLeave ? (event: React.MouseEvent) => { - onMouseLeave(event, { ...edge }); - } + onMouseLeave(event, { ...edge }); + } : undefined; const onKeyDown = (event: KeyboardEvent) => { diff --git a/packages/react/src/components/Edges/index.ts b/packages/react/src/components/Edges/index.ts index 21af9667..88b274eb 100644 --- a/packages/react/src/components/Edges/index.ts +++ b/packages/react/src/components/Edges/index.ts @@ -1,6 +1,8 @@ -// We distinguish between internal and exported edges -// The internal edges are used directly like custom edges and always get an id, source and target props -// If you import an edge from the library, the id is optional and source and target are not used at all +/* + * We distinguish between internal and exported edges + * The internal edges are used directly like custom edges and always get an id, source and target props + * If you import an edge from the library, the id is optional and source and target are not used at all + */ export { SimpleBezierEdge, SimpleBezierEdgeInternal } from './SimpleBezierEdge'; export { SmoothStepEdge, SmoothStepEdgeInternal } from './SmoothStepEdge'; diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index cc86e189..70e52dbe 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -228,8 +228,10 @@ function HandleComponent( connectingfrom: connectingFrom, connectingto: connectingTo, valid, - // shows where you can start a connection from - // and where you can end it while connecting + /* + * shows where you can start a connection from + * and where you can end it while connecting + */ connectionindicator: isConnectable && (!connectionInProcess || isPossibleEndHandle) && diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index bb136161..5c0ce08b 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -108,8 +108,10 @@ export function NodeWrapper({ const { selectNodesOnDrag, nodeDragThreshold } = store.getState(); if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) { - // this handler gets called by XYDrag on drag start when selectNodesOnDrag=true - // here we only need to call it when selectNodesOnDrag=false + /* + * this handler gets called by XYDrag on drag start when selectNodesOnDrag=true + * here we only need to call it when selectNodesOnDrag=false + */ handleNodeClick({ id, store, diff --git a/packages/react/src/components/NodeWrapper/useNodeObserver.ts b/packages/react/src/components/NodeWrapper/useNodeObserver.ts index 2c2910fd..de1d31e7 100644 --- a/packages/react/src/components/NodeWrapper/useNodeObserver.ts +++ b/packages/react/src/components/NodeWrapper/useNodeObserver.ts @@ -49,8 +49,10 @@ export function useNodeObserver({ useEffect(() => { if (nodeRef.current) { - // when the user programmatically changes the source or handle position, we need to update the internals - // to make sure the edges are updated correctly + /* + * when the user programmatically changes the source or handle position, we need to update the internals + * to make sure the edges are updated correctly + */ const typeChanged = prevType.current !== nodeType; const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition; const targetPosChanged = prevTargetPosition.current !== node.targetPosition; diff --git a/packages/react/src/components/NodeWrapper/utils.tsx b/packages/react/src/components/NodeWrapper/utils.tsx index c8c7d5c7..8c699c64 100644 --- a/packages/react/src/components/NodeWrapper/utils.tsx +++ b/packages/react/src/components/NodeWrapper/utils.tsx @@ -23,9 +23,9 @@ export const builtinNodeTypes: NodeTypes = { export function getNodeInlineStyleDimensions( node: InternalNode ): { - width: number | string | undefined; - height: number | string | undefined; -} { + width: number | string | undefined; + height: number | string | undefined; + } { if (node.internals.handleBounds === undefined) { return { width: node.width ?? node.initialWidth ?? node.style?.width, diff --git a/packages/react/src/components/Nodes/utils.ts b/packages/react/src/components/Nodes/utils.ts index 81939bbc..146e05f8 100644 --- a/packages/react/src/components/Nodes/utils.ts +++ b/packages/react/src/components/Nodes/utils.ts @@ -4,10 +4,12 @@ import { errorMessages } from '@xyflow/system'; import type { ReactFlowState } from '../../types'; -// this handler is called by -// 1. the click handler when node is not draggable or selectNodesOnDrag = false -// or -// 2. the on drag start handler when node is draggable and selectNodesOnDrag = true +/* + * this handler is called by + * 1. the click handler when node is not draggable or selectNodesOnDrag = false + * or + * 2. the on drag start handler when node is draggable and selectNodesOnDrag = true + */ export function handleNodeClick({ id, store, diff --git a/packages/react/src/components/NodesSelection/index.tsx b/packages/react/src/components/NodesSelection/index.tsx index ecaef17b..ae587b56 100644 --- a/packages/react/src/components/NodesSelection/index.tsx +++ b/packages/react/src/components/NodesSelection/index.tsx @@ -61,9 +61,9 @@ export function NodesSelection({ const onContextMenu = onSelectionContextMenu ? (event: MouseEvent) => { - const selectedNodes = store.getState().nodes.filter((n) => n.selected); - onSelectionContextMenu(event, selectedNodes); - } + const selectedNodes = store.getState().nodes.filter((n) => n.selected); + onSelectionContextMenu(event, selectedNodes); + } : undefined; const onKeyDown = (event: KeyboardEvent) => { diff --git a/packages/react/src/components/Panel/index.tsx b/packages/react/src/components/Panel/index.tsx index e6dd9edb..63695928 100644 --- a/packages/react/src/components/Panel/index.tsx +++ b/packages/react/src/components/Panel/index.tsx @@ -6,7 +6,8 @@ import { useStore } from '../../hooks/useStore'; import type { ReactFlowState } from '../../types'; export type PanelProps = HTMLAttributes & { - /** Set position of the panel + /** + * Set position of the panel * @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' */ position?: PanelPosition; diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index cac8df0e..e3a61ab2 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -94,9 +94,11 @@ const selector = (s: ReactFlowState) => ({ }); const initPrevValues = { - // these are values that are also passed directly to other components - // than the StoreUpdater. We can reduce the number of setStore calls - // by setting the same values here as prev fields. + /* + * these are values that are also passed directly to other components + * than the StoreUpdater. We can reduce the number of setStore calls + * by setting the same values here as prev fields. + */ translateExtent: infiniteExtent, nodeOrigin: defaultNodeOrigin, minZoom: 0.5, diff --git a/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx b/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx index 5b6194c5..17b5950c 100644 --- a/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx +++ b/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx @@ -42,9 +42,11 @@ const Marker = ({ ); }; -// when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore -// when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper -// that we can then use for creating our unique marker ids +/* + * when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore + * when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper + * that we can then use for creating our unique marker ids + */ const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => { const edges = useStore((s) => s.edges); const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions); diff --git a/packages/react/src/container/NodeRenderer/index.tsx b/packages/react/src/container/NodeRenderer/index.tsx index 7b0bd131..02126d73 100644 --- a/packages/react/src/container/NodeRenderer/index.tsx +++ b/packages/react/src/container/NodeRenderer/index.tsx @@ -44,29 +44,31 @@ function NodeRendererComponent(props: NodeRendererProps {nodeIds.map((nodeId) => { return ( - // The split of responsibilities between NodeRenderer and - // NodeComponentWrapper may appear weird. However, it’s designed to - // minimize the cost of updates when individual nodes change. - // - // For example, when you’re dragging a single node, that node gets - // updated multiple times per second. If `NodeRenderer` were to update - // every time, it would have to re-run the `nodes.map()` loop every - // time. This gets pricey with hundreds of nodes, especially if every - // loop cycle does more than just rendering a JSX element! - // - // As a result of this choice, we took the following implementation - // decisions: - // - NodeRenderer subscribes *only* to node IDs – and therefore - // rerender *only* when visible nodes are added or removed. - // - NodeRenderer performs all operations the result of which can be - // shared between nodes (such as creating the `ResizeObserver` - // instance, or subscribing to `selector`). This means extra prop - // drilling into `NodeComponentWrapper`, but it means we need to run - // these operations only once – instead of once per node. - // - Any operations that you’d normally write inside `nodes.map` are - // moved into `NodeComponentWrapper`. This ensures they are - // memorized – so if `NodeRenderer` *has* to rerender, it only - // needs to regenerate the list of nodes, nothing else. + /* + * The split of responsibilities between NodeRenderer and + * NodeComponentWrapper may appear weird. However, it’s designed to + * minimize the cost of updates when individual nodes change. + * + * For example, when you’re dragging a single node, that node gets + * updated multiple times per second. If `NodeRenderer` were to update + * every time, it would have to re-run the `nodes.map()` loop every + * time. This gets pricey with hundreds of nodes, especially if every + * loop cycle does more than just rendering a JSX element! + * + * As a result of this choice, we took the following implementation + * decisions: + * - NodeRenderer subscribes *only* to node IDs – and therefore + * rerender *only* when visible nodes are added or removed. + * - NodeRenderer performs all operations the result of which can be + * shared between nodes (such as creating the `ResizeObserver` + * instance, or subscribing to `selector`). This means extra prop + * drilling into `NodeComponentWrapper`, but it means we need to run + * these operations only once – instead of once per node. + * - Any operations that you’d normally write inside `nodes.map` are + * moved into `NodeComponentWrapper`. This ensures they are + * memorized – so if `NodeRenderer` *has* to rerender, it only + * needs to regenerate the list of nodes, nothing else. + */ key={nodeId} id={nodeId} diff --git a/packages/react/src/container/Pane/index.tsx b/packages/react/src/container/Pane/index.tsx index aeadde03..91459539 100644 --- a/packages/react/src/container/Pane/index.tsx +++ b/packages/react/src/container/Pane/index.tsx @@ -233,8 +233,10 @@ export function Pane({ (event.target as Partial)?.releasePointerCapture?.(event.pointerId); const { userSelectionRect } = store.getState(); - // We only want to trigger click functions when in selection mode if - // the user did not move the mouse. + /* + * We only want to trigger click functions when in selection mode if + * the user did not move the mouse. + */ if (!userSelectionActive && userSelectionRect && event.target === container.current) { onClick?.(event); } @@ -246,8 +248,10 @@ export function Pane({ }); onSelectionEnd?.(event); - // If the user kept holding the selectionKey during the selection, - // we need to reset the selectionInProgress, so the next click event is not prevented + /* + * If the user kept holding the selectionKey during the selection, + * we need to reset the selectionInProgress, so the next click event is not prevented + */ if (selectionKeyPressed || selectionOnDrag) { selectionInProgress.current = false; } diff --git a/packages/react/src/container/ReactFlow/Wrapper.tsx b/packages/react/src/container/ReactFlow/Wrapper.tsx index 5856809a..71a84316 100644 --- a/packages/react/src/container/ReactFlow/Wrapper.tsx +++ b/packages/react/src/container/ReactFlow/Wrapper.tsx @@ -31,8 +31,10 @@ export function Wrapper({ const isWrapped = useContext(StoreContext); if (isWrapped) { - // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode - // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 + /* + * we need to wrap it with a fragment because it's not allowed for children to be a ReactNode + * https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 + */ return <>{children}; } diff --git a/packages/react/src/contexts/NodeIdContext.ts b/packages/react/src/contexts/NodeIdContext.ts index 28a14b78..b5811ad6 100644 --- a/packages/react/src/contexts/NodeIdContext.ts +++ b/packages/react/src/contexts/NodeIdContext.ts @@ -4,6 +4,34 @@ export const NodeIdContext = createContext(null); export const Provider = NodeIdContext.Provider; export const Consumer = NodeIdContext.Consumer; +/** + * You can use this hook to get the id of the node it is used inside. It is useful + *if you need the node's id deeper in the render tree but don't want to manually + *drill down the id as a prop. + * + * @public + * @returns id of the node + * + * @example + *```jsx + *import { useNodeId } from '@xyflow/react'; + * + *export default function CustomNode() { + * return ( + *
+ * This node has an id of + * + *
+ * ); + *} + * + *function NodeIdDisplay() { + * const nodeId = useNodeId(); + * + * return {nodeId}; + *} + *``` + */ export const useNodeId = (): string | null => { const nodeId = useContext(NodeIdContext); return nodeId; diff --git a/packages/react/src/hooks/useConnection.ts b/packages/react/src/hooks/useConnection.ts index 94db5140..8992d767 100644 --- a/packages/react/src/hooks/useConnection.ts +++ b/packages/react/src/hooks/useConnection.ts @@ -24,9 +24,28 @@ function getSelector {connection ? `Someone is trying to make a connection from ${connection.fromNode} to this one.` : 'There are currently no incoming connections!'} + * + * + * ); + * } + * ``` + * * @returns ConnectionState */ export function useConnection>>( diff --git a/packages/react/src/hooks/useEdges.ts b/packages/react/src/hooks/useEdges.ts index 3ed9de39..731bf845 100644 --- a/packages/react/src/hooks/useEdges.ts +++ b/packages/react/src/hooks/useEdges.ts @@ -6,10 +6,22 @@ import type { Edge, ReactFlowState } from '../types'; const edgesSelector = (state: ReactFlowState) => state.edges; /** - * Hook for getting the current edges from the store. + * This hook returns an array of the current edges. Components that use this hook + *will re-render **whenever any edge changes**. * * @public * @returns An array of edges + * + * @example + * ```tsx + *import { useEdges } from '@xyflow/react'; + * + *export default function () { + * const edges = useEdges(); + * + * return
There are currently {edges.length} edges!
; + *} + *``` */ export function useEdges(): EdgeType[] { const edges = useStore(edgesSelector, shallow) as EdgeType[]; diff --git a/packages/react/src/hooks/useInternalNode.ts b/packages/react/src/hooks/useInternalNode.ts index d16d697b..bfdc74f9 100644 --- a/packages/react/src/hooks/useInternalNode.ts +++ b/packages/react/src/hooks/useInternalNode.ts @@ -5,11 +5,31 @@ import { useStore } from './useStore'; import type { InternalNode, Node } from '../types'; /** - * Hook for getting an internal node by id + * This hook returns the internal representation of a specific node. Components that use this hook + *will re-render **whenever the node changes**, including when a node is selected + *or moved. * * @public * @param id - id of the node * @returns array with visible node ids + * + * @example + * ```tsx + *import { useInternalNode } from '@xyflow/react'; + * + *export default function () { + * const internalNode = useInternalNode('node-1'); + * const absolutePosition = internalNode.internals.positionAbsolute; + * + * return ( + *
+ * The absolute position of the node is at: + *

x: {absolutePosition.x}

+ *

y: {absolutePosition.y}

+ *
+ * ); + *} + *``` */ export function useInternalNode(id: string): InternalNode | undefined { const node = useStore( diff --git a/packages/react/src/hooks/useKeyPress.ts b/packages/react/src/hooks/useKeyPress.ts index 8bce0564..6f2d01c3 100644 --- a/packages/react/src/hooks/useKeyPress.ts +++ b/packages/react/src/hooks/useKeyPress.ts @@ -13,18 +13,38 @@ export type UseKeyPressOptions = { const defaultDoc = typeof document !== 'undefined' ? document : null; /** - * Hook for handling key events. + * This hook lets you listen for specific key codes and tells you whether they are + *currently pressed or not. * * @public * @param param.keyCode - The key code (string or array of strings) to use * @param param.options - Options * @returns boolean + * + * @example + * ```tsx + *import { useKeyPress } from '@xyflow/react'; + * + *export default function () { + * const spacePressed = useKeyPress('Space'); + * const cmdAndSPressed = useKeyPress(['Meta+s', 'Strg+s']); + * + * return ( + *
+ * {spacePressed &&

Space pressed!

} + * {cmdAndSPressed &&

Cmd + S pressed!

} + *
+ * ); + *} + *``` */ export function useKeyPress( - // the keycode can be a string 'a' or an array of strings ['a', 'a+d'] - // a string means a single key 'a' or a combination when '+' is used 'a+d' - // an array means different possibilites. Explainer: ['a', 'd+s'] here the - // user can use the single key 'a' or the combination 'd' + 's' + /* + * the keycode can be a string 'a' or an array of strings ['a', 'a+d'] + * a string means a single key 'a' or a combination when '+' is used 'a+d' + * an array means different possibilites. Explainer: ['a', 'd+s'] here the + * user can use the single key 'a' or the combination 'd' + 's' + */ keyCode: KeyCode | null = null, options: UseKeyPressOptions = { target: defaultDoc, actInsideInputWithModifier: true } ): boolean { @@ -36,20 +56,24 @@ export function useKeyPress( // we need to remember the pressed keys in order to support combinations const pressedKeys = useRef(new Set([])); - // keyCodes = array with single keys [['a']] or key combinations [['a', 's']] - // keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft'] - // used to check if we store event.code or event.key. When the code is in the list of keysToWatch - // we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft" - // and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when - // we can't find it in the list of keysToWatch. + /* + * keyCodes = array with single keys [['a']] or key combinations [['a', 's']] + * keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft'] + * used to check if we store event.code or event.key. When the code is in the list of keysToWatch + * we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft" + * and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when + * we can't find it in the list of keysToWatch. + */ const [keyCodes, keysToWatch] = useMemo<[Array, Keys]>(() => { if (keyCode !== null) { const keyCodeArr = Array.isArray(keyCode) ? keyCode : [keyCode]; const keys = keyCodeArr .filter((kc) => typeof kc === 'string') - // we first replace all '+' with '\n' which we will use to split the keys on - // then we replace '\n\n' with '\n+', this way we can also support the combination 'key++' - // in the end we simply split on '\n' to get the key array + /* + * we first replace all '+' with '\n' which we will use to split the keys on + * then we replace '\n\n' with '\n+', this way we can also support the combination 'key++' + * in the end we simply split on '\n' to get the key array + */ .map((kc) => kc.replace('+', '\n').replace('\n\n', '\n+').split('\n')); const keysFlat = keys.reduce((res: Keys, item) => res.concat(...item), []); @@ -133,12 +157,16 @@ export function useKeyPress( function isMatchingKey(keyCodes: Array, pressedKeys: PressedKeys, isUp: boolean): boolean { return ( keyCodes - // we only want to compare same sizes of keyCode definitions - // and pressed keys. When the user specified 'Meta' as a key somewhere - // this would also be truthy without this filter when user presses 'Meta' + 'r' + /* + * we only want to compare same sizes of keyCode definitions + * and pressed keys. When the user specified 'Meta' as a key somewhere + * this would also be truthy without this filter when user presses 'Meta' + 'r' + */ .filter((keys) => isUp || keys.length === pressedKeys.size) - // since we want to support multiple possibilities only one of the - // combinations need to be part of the pressed keys + /* + * since we want to support multiple possibilities only one of the + * combinations need to be part of the pressed keys + */ .some((keys) => keys.every((k) => pressedKeys.has(k))) ); } diff --git a/packages/react/src/hooks/useMoveSelectedNodes.ts b/packages/react/src/hooks/useMoveSelectedNodes.ts index 52503c04..0b2dacc9 100644 --- a/packages/react/src/hooks/useMoveSelectedNodes.ts +++ b/packages/react/src/hooks/useMoveSelectedNodes.ts @@ -22,8 +22,10 @@ export function useMoveSelectedNodes() { const nodeUpdates = new Map(); const isSelected = selectedAndDraggable(nodesDraggable); - // by default a node moves 5px on each key press - // if snap grid is enabled, we use that for the velocity + /* + * by default a node moves 5px on each key press + * if snap grid is enabled, we use that for the velocity + */ const xVelo = snapToGrid ? snapGrid[0] : 5; const yVelo = snapToGrid ? snapGrid[1] : 5; diff --git a/packages/react/src/hooks/useNodeConnections.ts b/packages/react/src/hooks/useNodeConnections.ts index c83a7366..60ba894c 100644 --- a/packages/react/src/hooks/useNodeConnections.ts +++ b/packages/react/src/hooks/useNodeConnections.ts @@ -22,7 +22,7 @@ type UseNodeConnectionsParams = { }; /** - * Hook to retrieve all edges connected to a node. Can be filtered by handle type and id. + * This hook returns an array of connections on a specific node, handle type ('source', 'target') or handle ID. * * @public * @param param.id - node id - optional if called inside a custom node @@ -31,6 +31,22 @@ type UseNodeConnectionsParams = { * @param param.onConnect - gets called when a connection is established * @param param.onDisconnect - gets called when a connection is removed * @returns an array with connections + * + * @example + * ```jsx + *import { useNodeConnections } from '@xyflow/react'; + * + *export default function () { + * const connections = useNodeConnections({ + * type: 'target', + * handleId: 'my-handle', + * }); + * + * return ( + *
There are currently {connections.length} incoming connections!
+ * ); + *} + *``` */ export function useNodeConnections({ id, diff --git a/packages/react/src/hooks/useNodes.ts b/packages/react/src/hooks/useNodes.ts index 5e558e90..c64f7fb1 100644 --- a/packages/react/src/hooks/useNodes.ts +++ b/packages/react/src/hooks/useNodes.ts @@ -6,10 +6,23 @@ import type { Node, ReactFlowState } from '../types'; const nodesSelector = (state: ReactFlowState) => state.nodes; /** - * Hook for getting the current nodes from the store. + * This hook returns an array of the current nodes. Components that use this hook + *will re-render **whenever any node changes**, including when a node is selected + *or moved. * * @public * @returns An array of nodes + * + * @example + * ```jsx + *import { useNodes } from '@xyflow/react'; + * + *export default function() { + * const nodes = useNodes(); + * + * return
There are currently {nodes.length} nodes!
; + *} + *``` */ export function useNodes(): NodeType[] { const nodes = useStore(nodesSelector, shallow) as NodeType[]; diff --git a/packages/react/src/hooks/useNodesData.ts b/packages/react/src/hooks/useNodesData.ts index 7472cbf7..1d2aa37e 100644 --- a/packages/react/src/hooks/useNodesData.ts +++ b/packages/react/src/hooks/useNodesData.ts @@ -5,12 +5,24 @@ import { useStore } from '../hooks/useStore'; import type { Node } from '../types'; /** - * Hook for receiving data of one or multiple nodes + * This hook lets you subscribe to changes of a specific nodes `data` object. * * @public * @param nodeId - The id (or ids) of the node to get the data from - * @param guard - Optional guard function to narrow down the node type * @returns An object (or array of object) with {id, type, data} representing each node + * + * @example + * + *```jsx + *import { useNodesData } from '@xyflow/react'; + * + *export default function() { + * const nodeData = useNodesData('nodeId-1'); + * const nodesData = useNodesData(['nodeId-1', 'nodeId-2']); + * + * return null; + *} + *``` */ export function useNodesData( nodeId: string diff --git a/packages/react/src/hooks/useNodesEdgesState.ts b/packages/react/src/hooks/useNodesEdgesState.ts index fae87c02..b9722d80 100644 --- a/packages/react/src/hooks/useNodesEdgesState.ts +++ b/packages/react/src/hooks/useNodesEdgesState.ts @@ -4,11 +4,41 @@ import { applyNodeChanges, applyEdgeChanges } from '../utils/changes'; import type { Node, Edge, OnNodesChange, OnEdgesChange } from '../types'; /** - * Hook for managing the state of nodes - should only be used for prototyping / simple use cases. + * This hook makes it easy to prototype a controlled flow where you manage the + *state of nodes and edges outside the `ReactFlowInstance`. You can think of it + *like React's `useState` hook with an additional helper callback. * * @public * @param initialNodes * @returns an array [nodes, setNodes, onNodesChange] + * @example + * + *```tsx + *import { ReactFlow, useNodesState, useEdgesState } from '@xyflow/react'; + * + *const initialNodes = []; + *const initialEdges = []; + * + *export default function () { + * const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + * const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + * + * return ( + * + * ); + *} + *``` + * + *@remarks This hook was created to make prototyping easier and our documentation + *examples clearer. Although it is OK to use this hook in production, in + *practice you may want to use a more sophisticated state management solution + *like Zustand {@link https://reactflow.dev/docs/guides/state-management/} instead. + * */ export function useNodesState( initialNodes: NodeType[] @@ -23,11 +53,41 @@ export function useNodesState( } /** - * Hook for managing the state of edges - should only be used for prototyping / simple use cases. + * This hook makes it easy to prototype a controlled flow where you manage the + *state of nodes and edges outside the `ReactFlowInstance`. You can think of it + *like React's `useState` hook with an additional helper callback. * * @public * @param initialEdges * @returns an array [edges, setEdges, onEdgesChange] + * @example + * + *```tsx + *import { ReactFlow, useNodesState, useEdgesState } from '@xyflow/react'; + * + *const initialNodes = []; + *const initialEdges = []; + * + *export default function () { + * const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + * const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + * + * return ( + * + * ); + *} + *``` + * + * @remarks This hook was created to make prototyping easier and our documentation + *examples clearer. Although it is OK to use this hook in production, in + *practice you may want to use a more sophisticated state management solution + *like Zustand {@link https://reactflow.dev/docs/guides/state-management/} instead. + * */ export function useEdgesState( initialEdges: EdgeType[] diff --git a/packages/react/src/hooks/useNodesInitialized.ts b/packages/react/src/hooks/useNodesInitialized.ts index 98bfa6b7..38f15a60 100644 --- a/packages/react/src/hooks/useNodesInitialized.ts +++ b/packages/react/src/hooks/useNodesInitialized.ts @@ -1,6 +1,7 @@ +import { nodeHasDimensions } from '@xyflow/system'; + import { useStore } from './useStore'; import type { ReactFlowState } from '../types'; -import { nodeHasDimensions } from '@xyflow/system'; export type UseNodesInitializedOptions = { includeHiddenNodes?: boolean; @@ -22,18 +23,44 @@ const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) => return true; }; -const defaultOptions = { - includeHiddenNodes: false, -}; - /** - * Hook which returns true when all nodes are initialized. + * This hook tells you whether all the nodes in a flow have been measured and given + *a width and height. When you add a node to the flow, this hook will return + *`false` and then `true` again once the node has been measured. * * @public * @param options.includeHiddenNodes - defaults to false * @returns boolean indicating whether all nodes are initialized + * + * @example + * ```jsx + *import { useReactFlow, useNodesInitialized } from '@xyflow/react'; + *import { useEffect, useState } from 'react'; + * + *const options = { + * includeHiddenNodes: false, + *}; + * + *export default function useLayout() { + * const { getNodes } = useReactFlow(); + * const nodesInitialized = useNodesInitialized(options); + * const [layoutedNodes, setLayoutedNodes] = useState(getNodes()); + * + * useEffect(() => { + * if (nodesInitialized) { + * setLayoutedNodes(yourLayoutingFunction(getNodes())); + * } + * }, [nodesInitialized]); + * + * return layoutedNodes; + *} + *``` */ -export function useNodesInitialized(options: UseNodesInitializedOptions = defaultOptions): boolean { +export function useNodesInitialized( + options: UseNodesInitializedOptions = { + includeHiddenNodes: false, + } +): boolean { const initialized = useStore(selector(options)); return initialized; diff --git a/packages/react/src/hooks/useOnSelectionChange.ts b/packages/react/src/hooks/useOnSelectionChange.ts index 8e08191b..65cad6fd 100644 --- a/packages/react/src/hooks/useOnSelectionChange.ts +++ b/packages/react/src/hooks/useOnSelectionChange.ts @@ -8,10 +8,42 @@ export type UseOnSelectionChangeOptions = { }; /** - * Hook for registering an onSelectionChange handler. + * This hook lets you listen for changes to both node and edge selection. As the + *name implies, the callback you provide will be called whenever the selection of + *_either_ nodes or edges changes. * * @public * @param params.onChange - The handler to register + * + * @example + * ```jsx + *import { useState } from 'react'; + *import { ReactFlow, useOnSelectionChange } from '@xyflow/react'; + * + *function SelectionDisplay() { + * const [selectedNodes, setSelectedNodes] = useState([]); + * const [selectedEdges, setSelectedEdges] = useState([]); + * + * // the passed handler has to be memoized, otherwise the hook will not work correctly + * const onChange = useCallback(({ nodes, edges }) => { + * setSelectedNodes(nodes.map((node) => node.id)); + * setSelectedEdges(edges.map((edge) => edge.id)); + * }, []); + * + * useOnSelectionChange({ + * onChange, + * }); + * + * return ( + *
+ *

Selected nodes: {selectedNodes.join(', ')}

+ *

Selected edges: {selectedEdges.join(', ')}

+ *
+ * ); + *} + *``` + * + * @remarks You need to memoize the passed `onChange` handler, otherwise the hook will not work correctly. */ export function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) { const store = useStoreApi(); diff --git a/packages/react/src/hooks/useOnViewportChange.ts b/packages/react/src/hooks/useOnViewportChange.ts index 61e4d64f..c82eb344 100644 --- a/packages/react/src/hooks/useOnViewportChange.ts +++ b/packages/react/src/hooks/useOnViewportChange.ts @@ -10,12 +10,30 @@ export type UseOnViewportChangeOptions = { }; /** - * Hook for registering an onViewportChange handler. + * The `useOnViewportChange` hook lets you listen for changes to the viewport such + *as panning and zooming. You can provide a callback for each phase of a viewport + *change: `onStart`, `onChange`, and `onEnd`. * * @public * @param params.onStart - gets called when the viewport starts changing * @param params.onChange - gets called when the viewport changes * @param params.onEnd - gets called when the viewport stops changing + * + * @example + * ```jsx + *import { useCallback } from 'react'; + *import { useOnViewportChange } from '@xyflow/react'; + * + *function ViewportChangeLogger() { + * useOnViewportChange({ + * onStart: (viewport: Viewport) => console.log('start', viewport), + * onChange: (viewport: Viewport) => console.log('change', viewport), + * onEnd: (viewport: Viewport) => console.log('end', viewport), + * }); + * + * return null; + *} + *``` */ export function useOnViewportChange({ onStart, onChange, onEnd }: UseOnViewportChangeOptions) { const store = useStoreApi(); diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 3d857cfa..57b2e001 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -20,10 +20,33 @@ import type { ReactFlowInstance, Node, Edge, InternalNode, ReactFlowState, Gener const selector = (s: ReactFlowState) => !!s.panZoom; /** - * Hook for accessing the ReactFlow instance. + * This hook returns a ReactFlowInstance that can be used to update nodes and edges, manipulate the viewport, or query the current state of the flow. * * @public * @returns ReactFlowInstance + * + * @example + * ```jsx + *import { useCallback, useState } from 'react'; + *import { useReactFlow } from '@xyflow/react'; + * + *export function NodeCounter() { + * const reactFlow = useReactFlow(); + * const [count, setCount] = useState(0); + * const countNodes = useCallback(() => { + * setCount(reactFlow.getNodes().length); + * // you need to pass it as a dependency if you are using it with useEffect or useCallback + * // because at the first render, it's not initialized yet and some functions might not work. + * }, [reactFlow]); + * + * return ( + *
+ * + *

There are {count} nodes in the flow.

+ *
+ * ); + *} + *``` */ export function useReactFlow(): ReactFlowInstance< NodeType, diff --git a/packages/react/src/hooks/useStore.ts b/packages/react/src/hooks/useStore.ts index 88c1abad..e34e2c36 100644 --- a/packages/react/src/hooks/useStore.ts +++ b/packages/react/src/hooks/useStore.ts @@ -9,7 +9,9 @@ import type { Edge, Node, ReactFlowState } from '../types'; const zustandErrorMessage = errorMessages['error001'](); /** - * Hook for accessing the internal store. Should only be used in rare cases. + * This hook can be used to subscribe to internal state changes of the React Flow + *component. The `useStore` hook is re-exported from the [Zustand](https://github.com/pmndrs/zustand) + *state management library, so you should check out their docs for more details. * * @public * @param selector @@ -17,8 +19,13 @@ const zustandErrorMessage = errorMessages['error001'](); * @returns The selected state slice * * @example - * const nodes = useStore((state: ReactFlowState) => state.nodes); + * ```ts + * const nodes = useStore((state) => state.nodes); + * ``` * + * @remarks This hook should only be used if there is no other way to access the internal + *state. For many of the common use cases, there are dedicated hooks available + *such as {@link useReactFlow}, {@link useViewport}, etc. */ function useStore( selector: (state: ReactFlowState) => StateSlice, @@ -33,6 +40,20 @@ function useStore( return useZustandStore(store, selector, equalityFn); } +/** + * In some cases, you might need to access the store directly. This hook returns the store object which can be used on demand to access the state or dispatch actions. + * + * @returns The store object + * + * @example + * ```ts + * const store = useStoreApi(); + * ``` + * + * @remarks This hook should only be used if there is no other way to access the internal + *state. For many of the common use cases, there are dedicated hooks available + *such as {@link useReactFlow}, {@link useViewport}, etc. + */ function useStoreApi() { const store = useContext(StoreContext) as UseBoundStoreWithEqualityFn< StoreApi> diff --git a/packages/react/src/hooks/useUpdateNodeInternals.ts b/packages/react/src/hooks/useUpdateNodeInternals.ts index 6486afd8..4463ed2a 100644 --- a/packages/react/src/hooks/useUpdateNodeInternals.ts +++ b/packages/react/src/hooks/useUpdateNodeInternals.ts @@ -4,10 +4,48 @@ import type { UpdateNodeInternals, InternalNodeUpdate } from '@xyflow/system'; import { useStoreApi } from '../hooks/useStore'; /** - * Hook for updating node internals. + * When you programmatically add or remove handles to a node or update a node's + *handle position, you need to let React Flow know about it using this hook. This + *will update the internal dimensions of the node and properly reposition handles + *on the canvas if necessary. * * @public * @returns function for updating node internals + * + * @example + * ```jsx + *import { useCallback, useState } from 'react'; + *import { Handle, useUpdateNodeInternals } from '@xyflow/react'; + * + *export default function RandomHandleNode({ id }) { + * const updateNodeInternals = useUpdateNodeInternals(); + * const [handleCount, setHandleCount] = useState(0); + * const randomizeHandleCount = useCallback(() => { + * setHandleCount(Math.floor(Math.random() * 10)); + * updateNodeInternals(id); + * }, [id, updateNodeInternals]); + * + * return ( + * <> + * {Array.from({ length: handleCount }).map((_, index) => ( + * + * ))} + * + *
+ * + *

There are {handleCount} handles on this node.

+ *
+ * + * ); + *} + *``` + * @remarks This hook can only be used in a component that is a child of a + *{@link ReactFlowProvider} or a {@link ReactFlow} component. */ export function useUpdateNodeInternals(): UpdateNodeInternals { const store = useStoreApi(); diff --git a/packages/react/src/hooks/useViewport.ts b/packages/react/src/hooks/useViewport.ts index 0b9758c1..a892e5a8 100644 --- a/packages/react/src/hooks/useViewport.ts +++ b/packages/react/src/hooks/useViewport.ts @@ -11,10 +11,33 @@ const viewportSelector = (state: ReactFlowState) => ({ }); /** - * Hook for getting the current viewport from the store. + * The `useViewport` hook is a convenient way to read the current state of the + *{@link Viewport} in a component. Components that use this hook + *will re-render **whenever the viewport changes**. * * @public * @returns The current viewport + * + * @example + * + *```jsx + *import { useViewport } from '@xyflow/react'; + * + *export default function ViewportDisplay() { + * const { x, y, zoom } = useViewport(); + * + * return ( + *
+ *

+ * The viewport is currently at ({x}, {y}) and zoomed to {zoom}. + *

+ *
+ * ); + *} + *``` + * + * @remarks This hook can only be used in a component that is a child of a + *{@link ReactFlowProvider} or a {@link ReactFlow} component. */ export function useViewport(): Viewport { const viewport = useStore(viewportSelector, shallow); diff --git a/packages/react/src/hooks/useVisibleNodeIds.ts b/packages/react/src/hooks/useVisibleNodeIds.ts index e8d14c8e..a0006940 100644 --- a/packages/react/src/hooks/useVisibleNodeIds.ts +++ b/packages/react/src/hooks/useVisibleNodeIds.ts @@ -8,8 +8,8 @@ import type { Node, ReactFlowState } from '../types'; const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => { return onlyRenderVisible ? getNodesInside(s.nodeLookup, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map( - (node) => node.id - ) + (node) => node.id + ) : Array.from(s.nodeLookup.keys()); }; diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 2466baa5..0f07d0f1 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -47,12 +47,14 @@ const createStore = ({ ...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent, defaultNodes, defaultEdges }), setNodes: (nodes: Node[]) => { const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get(); - // setNodes() is called exclusively in response to user actions: - // - either when the `` prop is updated in the controlled ReactFlow setup, - // - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup. - // - // When this happens, we take the note objects passed by the user and extend them with fields - // relevant for internal React Flow operations. + /* + * setNodes() is called exclusively in response to user actions: + * - either when the `` prop is updated in the controlled ReactFlow setup, + * - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup. + * + * When this happens, we take the note objects passed by the user and extend them with fields + * relevant for internal React Flow operations. + */ adoptUserNodes(nodes, nodeLookup, parentLookup, { nodeOrigin, nodeExtent, @@ -81,9 +83,11 @@ const createStore = ({ set({ hasDefaultEdges: true }); } }, - // Every node gets registerd at a ResizeObserver. Whenever a node - // changes its dimensions, this function is called to measure the - // new dimensions and update the nodes. + /* + * Every node gets registerd at a ResizeObserver. Whenever a node + * changes its dimensions, this function is called to measure the + * new dimensions and update the nodes. + */ updateNodeInternals: (updates, params = { triggerFitView: true }) => { const { triggerNodeChanges, @@ -125,11 +129,13 @@ const createStore = ({ }); } - // here we are cirmumventing the onNodesChange handler - // in order to be able to display nodes even if the user - // has not provided an onNodesChange handler. - // Nodes are only rendered if they have a width and height - // attribute which they get from this handler. + /* + * here we are cirmumventing the onNodesChange handler + * in order to be able to display nodes even if the user + * has not provided an onNodesChange handler. + * Nodes are only rendered if they have a width and height + * attribute which they get from this handler. + */ set({ fitViewDone: nextFitViewDone }); } else { // we always want to trigger useStore calls whenever updateNodeInternals is called @@ -155,9 +161,9 @@ const createStore = ({ type: 'position', position: expandParent ? { - x: Math.max(0, dragItem.position.x), - y: Math.max(0, dragItem.position.y), - } + x: Math.max(0, dragItem.position.x), + y: Math.max(0, dragItem.position.y), + } : dragItem.position, dragging, }; @@ -248,8 +254,10 @@ const createStore = ({ const nodeChanges = nodesToUnselect.map((n) => { const internalNode = nodeLookup.get(n.id); if (internalNode) { - // we need to unselect the internal node that was selected previously before we - // send the change to the user to prevent it to be selected while dragging the new node + /* + * we need to unselect the internal node that was selected previously before we + * send the change to the user to prevent it to be selected while dragging the new node + */ internalNode.selected = false; } @@ -342,8 +350,10 @@ const createStore = ({ options ); }, - // we can't call an asnychronous function in updateNodeInternals - // for that we created this sync version of fitView + /* + * we can't call an asnychronous function in updateNodeInternals + * for that we created this sync version of fitView + */ fitViewSync: (options?: FitViewOptions): boolean => { const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get(); diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index d04875d1..2f318174 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -52,7 +52,8 @@ import type { */ export interface ReactFlowProps extends Omit, 'onError'> { - /** An array of nodes to render in a controlled flow. + /** + * An array of nodes to render in a controlled flow. * @example * const nodes = [ * { @@ -64,7 +65,8 @@ export interface ReactFlowProps; onReconnectStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void; onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void; - /** This event handler is called when a Node is updated + /** + * This event handler is called when a Node is updated * @example // Use NodesState hook to create edges and get onNodesChange handler * import ReactFlow, { useNodesState } from '@xyflow/react'; * const [edges, setNodes, onNodesChange] = useNodesState(initialNodes); @@ -149,7 +153,8 @@ export interface ReactFlowProps) */ onNodesChange?: OnNodesChange; - /** This event handler is called when a Edge is updated + /** + * This event handler is called when a Edge is updated * @example // Use EdgesState hook to create edges and get onEdgesChange handler * import ReactFlow, { useEdgesState } from '@xyflow/react'; * const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -181,7 +186,8 @@ export interface ReactFlowProps void; onSelectionEnd?: (event: ReactMouseEvent) => void; onSelectionContextMenu?: (event: ReactMouseEvent, nodes: NodeType[]) => void; - /** When a connection line is completed and two nodes are connected by the user, this event fires with the new connection. + /** + * When a connection line is completed and two nodes are connected by the user, this event fires with the new connection. * * You can use the addEdge utility to convert the connection to a complete edge. * @example // Use helper function to update edges onConnect @@ -223,17 +229,20 @@ export interface ReactFlowProps void; /** This event handler gets called when mouse leaves the pane */ onPaneMouseLeave?: (event: ReactMouseEvent) => void; - /** Distance that the mouse can move between mousedown/up that will trigger a click + /** + * Distance that the mouse can move between mousedown/up that will trigger a click * @default 0 */ paneClickDistance?: number; - /** Distance that the mouse can move between mousedown/up that will trigger a click + /** + * Distance that the mouse can move between mousedown/up that will trigger a click * @default 0 */ nodeClickDistance?: number; /** This handler gets called before the user deletes nodes or edges and provides a way to abort the deletion by returning false. */ onBeforeDelete?: OnBeforeDelete; - /** Custom node types to be available in a flow. + /** + * Custom node types to be available in a flow. * * React Flow matches a node's type to a component in the nodeTypes object. * @example @@ -242,7 +251,8 @@ export interface ReactFlowProps void; - /** By default the viewport extends infinitely. You can use this prop to set a boundary. + /** + * By default the viewport extends infinitely. You can use this prop to set a boundary. * * The first pair of coordinates is the top left boundary and the second pair is the bottom right. * @example [[-1000, -10000], [1000, 1000]] */ translateExtent?: CoordinateExtent; - /** Disabling this prop will allow the user to scroll the page even when their pointer is over the flow. + /** + * Disabling this prop will allow the user to scroll the page even when their pointer is over the flow. * @default true */ preventScrolling?: boolean; - /** By default nodes can be placed on an infinite flow. You can use this prop to set a boundary. + /** + * By default nodes can be placed on an infinite flow. You can use this prop to set a boundary. * * The first pair of coordinates is the top left boundary and the second pair is the bottom right. * @example [[-1000, -10000], [1000, 1000]] */ nodeExtent?: CoordinateExtent; - /** Color of edge markers + /** + * Color of edge markers * @example "#b1b1b7" */ defaultMarkerColor?: string; @@ -402,17 +439,20 @@ export interface ReactFlowProps true */ isValidConnection?: IsValidConnection; - /** With a threshold greater than zero you can control the distinction between node drag and click events. + /** + * With a threshold greater than zero you can control the distinction between node drag and click events. * * If threshold equals 1, you need to drag the node 1 pixel before a drag event is fired. * @default 1 @@ -510,12 +563,14 @@ export interface ReactFlowProps(); const addItemChanges: any[] = []; @@ -26,15 +30,19 @@ function applyChanges(changes: any[], elements: any[]): any[] { addItemChanges.push(change); continue; } else if (change.type === 'remove' || change.type === 'replace') { - // For a 'remove' change we can safely ignore any other changes queued for - // the same element, it's going to be removed anyway! + /* + * For a 'remove' change we can safely ignore any other changes queued for + * the same element, it's going to be removed anyway! + */ changesMap.set(change.id, [change]); } else { const elementChanges = changesMap.get(change.id); if (elementChanges) { - // If we have some changes queued already, we can do a mutable update of - // that array and save ourselves some copying. + /* + * If we have some changes queued already, we can do a mutable update of + * that array and save ourselves some copying. + */ elementChanges.push(change); } else { changesMap.set(change.id, [change]); @@ -45,8 +53,10 @@ function applyChanges(changes: any[], elements: any[]): any[] { for (const element of elements) { const changes = changesMap.get(element.id); - // When there are no changes for an element we can just push it unmodified, - // no need to copy it. + /* + * When there are no changes for an element we can just push it unmodified, + * no need to copy it. + */ if (!changes) { updatedElements.push(element); continue; @@ -62,9 +72,11 @@ function applyChanges(changes: any[], elements: any[]): any[] { continue; } - // For other types of changes, we want to start with a shallow copy of the - // object so React knows this element has changed. Sequential changes will - /// each _mutate_ this object, so there's only ever one copy. + /** + * For other types of changes, we want to start with a shallow copy of the + * object so React knows this element has changed. Sequential changes will + * each _mutate_ this object, so there's only ever one copy. + */ const updatedElement = { ...element }; for (const change of changes) { @@ -74,8 +86,10 @@ function applyChanges(changes: any[], elements: any[]): any[] { updatedElements.push(updatedElement); } - // we need to wait for all changes to be applied before adding new items - // to be able to add them at the correct index + /* + * we need to wait for all changes to be applied before adding new items + * to be able to add them at the correct index + */ if (addItemChanges.length) { addItemChanges.forEach((change) => { if (change.index !== undefined) { @@ -134,21 +148,21 @@ function applyChange(change: any, element: any): any { * Drop in function that applies node changes to an array of nodes. * @public * @remarks Various events on the component can produce an {@link NodeChange} that describes how to update the edges of your flow in some way. - If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges. + *If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges. * @param changes - Array of changes to apply * @param nodes - Array of nodes to apply the changes to * @returns Array of updated nodes * @example * const onNodesChange = useCallback( - (changes) => { - setNodes((oldNodes) => applyNodeChanges(changes, oldNodes)); - }, - [setNodes], - ); - - return ( - - ); + * (changes) => { + * setNodes((oldNodes) => applyNodeChanges(changes, oldNodes)); + * }, + * [setNodes], + * ); + * + * return ( + * + * ); */ export function applyNodeChanges( changes: NodeChange[], @@ -161,21 +175,21 @@ export function applyNodeChanges( * Drop in function that applies edge changes to an array of edges. * @public * @remarks Various events on the component can produce an {@link EdgeChange} that describes how to update the edges of your flow in some way. - If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges. + *If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges. * @param changes - Array of changes to apply * @param edges - Array of edge to apply the changes to * @returns Array of updated edges * @example * const onEdgesChange = useCallback( - (changes) => { - setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges)); - }, - [setEdges], - ); - - return ( - - ); + * (changes) => { + * setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges)); + * }, + * [setEdges], + * ); + * + * return ( + * + * ); */ export function applyEdgeChanges( changes: EdgeChange[], @@ -205,9 +219,11 @@ export function getSelectionChanges( // we don't want to set all items to selected=false on the first selection if (!(item.selected === undefined && !willBeSelected) && item.selected !== willBeSelected) { if (mutateItem) { - // this hack is needed for nodes. When the user dragged a node, it's selected. - // When another node gets dragged, we need to deselect the previous one, - // in order to have only one selected node at a time - the onNodesChange callback comes too late here :/ + /* + * this hack is needed for nodes. When the user dragged a node, it's selected. + * When another node gets dragged, we need to deselect the previous one, + * in order to have only one selected node at a time - the onNodesChange callback comes too late here :/ + */ item.selected = willBeSelected; } changes.push(createSelectionChange(item.id, willBeSelected)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22541966..6ae9a41c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -428,6 +428,9 @@ importers: tooling/eslint-config: devDependencies: + '@stylistic/eslint-plugin': + specifier: ^3.1.0 + version: 3.1.0(eslint@8.57.0)(typescript@5.4.5) '@typescript-eslint/eslint-plugin': specifier: ^8.23.0 version: 8.23.0(@typescript-eslint/parser@8.23.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) @@ -1706,6 +1709,12 @@ packages: resolution: {integrity: sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==} engines: {node: '>=18'} + '@stylistic/eslint-plugin@3.1.0': + resolution: {integrity: sha512-pA6VOrOqk0+S8toJYhQGv2MWpQQR0QpeUo9AhNkC49Y26nxBQ/nH1rta9bUU1rPw2fJ1zZEMV5oCX5AazT7J2g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.40.0' + '@svelte-put/shortcut@3.1.1': resolution: {integrity: sha512-2L5EYTZXiaKvbEelVkg5znxqvfZGZai3m97+cAiUBhLZwXnGtviTDpHxOoZBsqz41szlfRMcamW/8o0+fbW3ZQ==} peerDependencies: @@ -2217,6 +2226,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -3290,6 +3304,10 @@ packages: esm-env@1.0.0: resolution: {integrity: sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==} + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4776,6 +4794,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -7959,6 +7981,18 @@ snapshots: '@sindresorhus/merge-streams@1.0.0': {} + '@stylistic/eslint-plugin@3.1.0(eslint@8.57.0)(typescript@5.4.5)': + dependencies: + '@typescript-eslint/utils': 8.23.0(eslint@8.57.0)(typescript@5.4.5) + eslint: 8.57.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + estraverse: 5.3.0 + picomatch: 4.0.2 + transitivePeerDependencies: + - supports-color + - typescript + '@svelte-put/shortcut@3.1.1(svelte@4.2.12)': dependencies: svelte: 4.2.12 @@ -8613,12 +8647,18 @@ snapshots: dependencies: acorn: 8.10.0 + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + acorn@8.10.0: {} acorn@8.11.2: {} acorn@8.11.3: {} + acorn@8.14.0: {} + aggregate-error@3.1.0: dependencies: clean-stack: 2.2.0 @@ -10257,6 +10297,12 @@ snapshots: esm-env@1.0.0: {} + espree@10.3.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 + espree@9.6.1: dependencies: acorn: 8.10.0 @@ -11961,6 +12007,8 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.2: {} + pify@2.3.0: {} pify@4.0.1: {} diff --git a/tooling/eslint-config/package.json b/tooling/eslint-config/package.json index 00da90da..2c664276 100644 --- a/tooling/eslint-config/package.json +++ b/tooling/eslint-config/package.json @@ -5,11 +5,12 @@ "license": "MIT", "main": "src/index.js", "devDependencies": { - "eslint-config-prettier": "^10.0.1", - "eslint-config-turbo": "^2.4.0", - "eslint-plugin-react": "^7.37.4", + "@stylistic/eslint-plugin": "^3.1.0", "@typescript-eslint/eslint-plugin": "^8.23.0", "@typescript-eslint/parser": "^8.23.0", - "eslint-plugin-prettier": "^4.2.1" + "eslint-config-prettier": "^10.0.1", + "eslint-config-turbo": "^2.4.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react": "^7.37.4" } } diff --git a/tooling/eslint-config/src/index.js b/tooling/eslint-config/src/index.js index f2ce34a0..7bd69b5a 100644 --- a/tooling/eslint-config/src/index.js +++ b/tooling/eslint-config/src/index.js @@ -13,7 +13,7 @@ module.exports = { 'turbo', 'prettier', ], - plugins: ['react', '@typescript-eslint'], + plugins: ['react', '@typescript-eslint', '@stylistic'], parserOptions: { ecmaFeatures: { jsx: true, @@ -28,5 +28,7 @@ module.exports = { }, rules: { '@typescript-eslint/no-non-null-assertion': 'off', + '@stylistic/indent': ['error', 2], + '@stylistic/multiline-comment-style': ['error', 'starred-block'], }, };