implemented new fitView logic

This commit is contained in:
peterkogo
2025-04-09 10:36:16 +02:00
parent 8b5284697c
commit 9383ba5554
132 changed files with 5506 additions and 2297 deletions
+24 -1
View File
@@ -24,9 +24,32 @@ function getSelector<NodeType extends Node = Node, SelectorReturn = ConnectionSt
return storeSelector;
}
/**
* Hook for accessing the connection state.
* The `useConnection` hook returns the current connection when there is an active
* connection interaction. If no connection interaction is active, it returns null
* for every property. A typical use case for this hook is to colorize handles
* based on a certain condition (e.g. if the connection is valid or not).
*
* @public
* @param connectionSelector - An optional selector function used to extract a slice of the
* `ConnectionState` data. Using a selector can prevent component re-renders where data you don't
* otherwise care about might change. If a selector is not provided, the entire `ConnectionState`
* object is returned unchanged.
* @example
*
* ```tsx
*import { useConnection } from '@xyflow/react';
*
*function App() {
* const connection = useConnection();
*
* return (
* <div> {connection ? `Someone is trying to make a connection from ${connection.fromNode} to this one.` : 'There are currently no incoming connections!'}
*
* </div>
* );
* }
* ```
*
* @returns ConnectionState
*/
export function useConnection<NodeType extends Node = Node, SelectorReturn = ConnectionState<InternalNode<NodeType>>>(
+14 -2
View File
@@ -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
* @returns An array of all edges currently in the flow.
*
* @example
* ```tsx
*import { useEdges } from '@xyflow/react';
*
*export default function () {
* const edges = useEdges();
*
* return <div>There are currently {edges.length} edges!</div>;
*}
*```
*/
export function useEdges<EdgeType extends Edge = Edge>(): EdgeType[] {
const edges = useStore(edgesSelector, shallow) as EdgeType[];
@@ -10,11 +10,16 @@ import {
import { useStore } from './useStore';
import { useNodeId } from '../contexts/NodeIdContext';
type useHandleConnectionsParams = {
type UseHandleConnectionsParams = {
/** What type of handle connections do you want to observe? */
type: HandleType;
/** The handle id (this is only needed if the node has multiple handles of the same type). */
id?: string | null;
/** If node id is not provided, the node id from the `NodeIdContext` is used. */
nodeId?: string;
/** Gets called when a connection is established. */
onConnect?: (connections: Connection[]) => void;
/** Gets called when a connection is removed. */
onDisconnect?: (connections: Connection[]) => void;
};
@@ -23,12 +28,7 @@ type useHandleConnectionsParams = {
*
* @public
* @deprecated Use `useNodeConnections` instead.
* @param param.type - handle type 'source' or 'target'
* @param param.nodeId - node id - if not provided, the node id from the NodeIdContext is used
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
* @param param.onConnect - gets called when a connection is established
* @param param.onDisconnect - gets called when a connection is removed
* @returns an array with handle connections
* @returns An array with handle connections.
*/
export function useHandleConnections({
type,
@@ -36,7 +36,7 @@ export function useHandleConnections({
nodeId,
onConnect,
onDisconnect,
}: useHandleConnectionsParams): HandleConnection[] {
}: UseHandleConnectionsParams): HandleConnection[] {
console.warn(
'[DEPRECATED] `useHandleConnections` is deprecated. Instead use `useNodeConnections` https://reactflow.dev/api-reference/hooks/useNodeConnections'
);
+23 -3
View File
@@ -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
* @param id - The ID of a node you want to observe.
* @returns The `InternalNode` object for the node with the given ID.
*
* @example
* ```tsx
*import { useInternalNode } from '@xyflow/react';
*
*export default function () {
* const internalNode = useInternalNode('node-1');
* const absolutePosition = internalNode.internals.positionAbsolute;
*
* return (
* <div>
* The absolute position of the node is at:
* <p>x: {absolutePosition.x}</p>
* <p>y: {absolutePosition.y}</p>
* </div>
* );
*}
*```
*/
export function useInternalNode<NodeType extends Node = Node>(id: string): InternalNode<NodeType> | undefined {
const node = useStore(
+71 -31
View File
@@ -6,25 +6,58 @@ type PressedKeys = Set<string>;
type KeyOrCode = 'key' | 'code';
export type UseKeyPressOptions = {
/**
* Listen to key presses on a specific element.
* @default document
*/
target?: Window | Document | HTMLElement | ShadowRoot | null;
/**
* You can use this flag to prevent triggering the key press hook when an input field is focused.
* @default true
*/
actInsideInputWithModifier?: boolean;
preventDefault?: boolean;
};
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
* @param options - Options
*
* @example
* ```tsx
*import { useKeyPress } from '@xyflow/react';
*
*export default function () {
* const spacePressed = useKeyPress('Space');
* const cmdAndSPressed = useKeyPress(['Meta+s', 'Strg+s']);
*
* return (
* <div>
* {spacePressed && <p>Space pressed!</p>}
* {cmdAndSPressed && <p>Cmd + S pressed!</p>}
* </div>
* );
*}
*```
*/
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 key code (string or array of strings) specifies which key(s) should trigger
* an action.
*
* A **string** can represent:
* - A **single key**, e.g. `'a'`
* - A **key combination**, using `'+'` to separate keys, e.g. `'a+d'`
*
* An **array of strings** represents **multiple possible key inputs**. For example, `['a', 'd+s']`
* means the user can press either the single key `'a'` or the combination of `'d'` and `'s'`.
* @default null
*/
keyCode: KeyCode | null = null,
options: UseKeyPressOptions = { target: defaultDoc, actInsideInputWithModifier: true }
): boolean {
@@ -36,20 +69,24 @@ export function useKeyPress(
// we need to remember the pressed keys in order to support combinations
const pressedKeys = useRef<PressedKeys>(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>, 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), []);
@@ -64,7 +101,7 @@ export function useKeyPress(
if (keyCode !== null) {
const downHandler = (event: KeyboardEvent) => {
modifierPressed.current = event.ctrlKey || event.metaKey || event.shiftKey;
modifierPressed.current = event.ctrlKey || event.metaKey || event.shiftKey || event.altKey;
const preventAction =
(!modifierPressed.current || (modifierPressed.current && !options.actInsideInputWithModifier)) &&
isInputDOMNode(event);
@@ -76,19 +113,18 @@ export function useKeyPress(
pressedKeys.current.add(event[keyOrCode]);
if (isMatchingKey(keyCodes, pressedKeys.current, false)) {
event.preventDefault();
const target = (event.composedPath?.()?.[0] || event.target) as Element | null;
const isInteractiveElement = target?.nodeName === 'BUTTON' || target?.nodeName === 'A';
if (options.preventDefault !== false && (modifierPressed.current || !isInteractiveElement)) {
event.preventDefault();
}
setKeyPressed(true);
}
};
const upHandler = (event: KeyboardEvent) => {
const preventAction =
(!modifierPressed.current || (modifierPressed.current && !options.actInsideInputWithModifier)) &&
isInputDOMNode(event);
if (preventAction) {
return false;
}
const keyOrCode = useKeyOrCode(event.code, keysToWatch);
if (isMatchingKey(keyCodes, pressedKeys.current, true)) {
@@ -133,12 +169,16 @@ export function useKeyPress(
function isMatchingKey(keyCodes: Array<Keys>, 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)))
);
}
@@ -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;
+24 -8
View File
@@ -14,23 +14,39 @@ import { useNodeId } from '../contexts/NodeIdContext';
const error014 = errorMessages['error014']();
type UseNodeConnectionsParams = {
/** ID of the node, filled in automatically if used inside custom node. */
id?: string;
/** What type of handle connections do you want to observe? */
handleType?: HandleType;
/** Filter by handle id (this is only needed if the node has multiple handles of the same type). */
handleId?: string;
/** Gets called when a connection is established. */
onConnect?: (connections: Connection[]) => void;
/** Gets called when a connection is removed. */
onDisconnect?: (connections: Connection[]) => void;
};
/**
* 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
* @param param.handleType - filter by handle type 'source' or 'target'
* @param param.handleId - filter by handle id (this is only needed if the node has multiple handles of the same type)
* @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
* @returns An array with connections.
*
* @example
* ```jsx
*import { useNodeConnections } from '@xyflow/react';
*
*export default function () {
* const connections = useNodeConnections({
* handleType: 'target',
* handleId: 'my-handle',
* });
*
* return (
* <div>There are currently {connections.length} incoming connections!</div>
* );
*}
*```
*/
export function useNodeConnections({
id,
@@ -57,7 +73,7 @@ export function useNodeConnections({
);
useEffect(() => {
// @todo dicuss if onConnect/onDisconnect should be called when the component mounts/unmounts
// @todo discuss if onConnect/onDisconnect should be called when the component mounts/unmounts
if (prevConnections.current && prevConnections.current !== connections) {
const _connections = connections ?? new Map();
handleConnectionChange(prevConnections.current, _connections, onDisconnect);
+15 -2
View File
@@ -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
* @returns An array of all nodes currently in the flow.
*
* @example
* ```jsx
*import { useNodes } from '@xyflow/react';
*
*export default function() {
* const nodes = useNodes();
*
* return <div>There are currently {nodes.length} nodes!</div>;
*}
*```
*/
export function useNodes<NodeType extends Node = Node>(): NodeType[] {
const nodes = useStore(nodesSelector, shallow) as NodeType[];
+19 -5
View File
@@ -5,17 +5,31 @@ 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
* @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<NodeType extends Node = Node>(
/** The id of the node to get the data from. */
nodeId: string
): Pick<NodeType, 'id' | 'type' | 'data'> | null;
export function useNodesData<NodeType extends Node = Node>(nodeIds: string[]): Pick<NodeType, 'id' | 'type' | 'data'>[];
export function useNodesData<NodeType extends Node = Node>(
/** The ids of the nodes to get the data from. */
nodeIds: string[]
): Pick<NodeType, 'id' | 'type' | 'data'>[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useNodesData(nodeIds: any): any {
const nodesData = useStore(
+96 -8
View File
@@ -4,15 +4,58 @@ 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]
* @returns
* - `nodes`: The current array of nodes. You might pass this directly to the `nodes` prop of your
* `<ReactFlow />` component, or you may want to manipulate it first to perform some layouting,
* for example.
* - `setNodes`: A function that you can use to update the nodes. You can pass it a new array of
* nodes or a callback that receives the current array of nodes and returns a new array of nodes.
* This is the same as the second element of the tuple returned by React's `useState` hook.
* - `onNodesChange`: A handy callback that can take an array of `NodeChanges` and update the nodes
* state accordingly. You'll typically pass this directly to the `onNodesChange` prop of your
* `<ReactFlow />` component.
* @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 (
* <ReactFlow
* nodes={nodes}
* edges={edges}
* onNodesChange={onNodesChange}
* onEdgesChange={onEdgesChange}
* />
* );
*}
*```
*
* @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<NodeType extends Node>(
initialNodes: NodeType[]
): [NodeType[], Dispatch<SetStateAction<NodeType[]>>, OnNodesChange<NodeType>] {
): [
//
nodes: NodeType[],
setNodes: Dispatch<SetStateAction<NodeType[]>>,
onNodesChange: OnNodesChange<NodeType>
] {
const [nodes, setNodes] = useState(initialNodes);
const onNodesChange: OnNodesChange<NodeType> = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
@@ -23,15 +66,60 @@ export function useNodesState<NodeType extends Node>(
}
/**
* 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]
* @returns
* - `edges`: The current array of edges. You might pass this directly to the `edges` prop of your
* `<ReactFlow />` component, or you may want to manipulate it first to perform some layouting,
* for example.
*
* - `setEdges`: A function that you can use to update the edges. You can pass it a new array of
* edges or a callback that receives the current array of edges and returns a new array of edges.
* This is the same as the second element of the tuple returned by React's `useState` hook.
*
* - `onEdgesChange`: A handy callback that can take an array of `EdgeChanges` and update the edges
* state accordingly. You'll typically pass this directly to the `onEdgesChange` prop of your
* `<ReactFlow />` component.
* @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 (
* <ReactFlow
* nodes={nodes}
* edges={edges}
* onNodesChange={onNodesChange}
* onEdgesChange={onEdgesChange}
* />
* );
*}
*```
*
* @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<EdgeType extends Edge = Edge>(
initialEdges: EdgeType[]
): [EdgeType[], Dispatch<SetStateAction<EdgeType[]>>, OnEdgesChange<EdgeType>] {
): [
//
edges: EdgeType[],
setEdges: Dispatch<SetStateAction<EdgeType[]>>,
onEdgesChange: OnEdgesChange<EdgeType>
] {
const [edges, setEdges] = useState(initialEdges);
const onEdgesChange: OnEdgesChange<EdgeType> = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
+38 -10
View File
@@ -1,8 +1,10 @@
import { useStore } from './useStore';
import type { ReactFlowState } from '../types';
import { nodeHasDimensions } from '@xyflow/system';
import { useStore } from './useStore';
import type { ReactFlowState } from '../types';
export type UseNodesInitializedOptions = {
/** @default false */
includeHiddenNodes?: boolean;
};
@@ -22,18 +24,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
* @returns Whether or not the nodes have been initialized by the `<ReactFlow />` component and
* given a width and height.
*
* @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;
@@ -1,20 +1,53 @@
import { useEffect } from 'react';
import { useStoreApi } from './useStore';
import type { OnSelectionChangeFunc } from '../types';
import type { OnSelectionChangeFunc, Node, Edge } from '../types';
export type UseOnSelectionChangeOptions = {
onChange: OnSelectionChangeFunc;
export type UseOnSelectionChangeOptions<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
/** The handler to register. */
onChange: OnSelectionChangeFunc<NodeType, EdgeType>;
};
/**
* 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 (
* <div>
* <p>Selected nodes: {selectedNodes.join(', ')}</p>
* <p>Selected edges: {selectedEdges.join(', ')}</p>
* </div>
* );
*}
*```
*
* @remarks You need to memoize the passed `onChange` handler, otherwise the hook will not work correctly.
*/
export function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) {
const store = useStoreApi();
export function useOnSelectionChange<NodeType extends Node = Node, EdgeType extends Edge = Edge>({
onChange,
}: UseOnSelectionChangeOptions<NodeType, EdgeType>) {
const store = useStoreApi<NodeType, EdgeType>();
useEffect(() => {
const nextOnSelectionChangeHandlers = [...store.getState().onSelectionChangeHandlers, onChange];
@@ -4,18 +4,35 @@ import type { OnViewportChange } from '@xyflow/system';
import { useStoreApi } from './useStore';
export type UseOnViewportChangeOptions = {
/** Gets called when the viewport starts changing. */
onStart?: OnViewportChange;
/** Gets called when the viewport changes. */
onChange?: OnViewportChange;
/** Gets called when the viewport stops changing. */
onEnd?: OnViewportChange;
};
/**
* 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();
+46 -6
View File
@@ -15,15 +15,44 @@ import useViewportHelper from './useViewportHelper';
import { useStore, useStoreApi } from './useStore';
import { useBatchContext } from '../components/BatchProvider';
import { elementToRemoveChange, isEdge, isNode } from '../utils';
import type { ReactFlowInstance, Node, Edge, InternalNode, ReactFlowState, GeneralHelpers } from '../types';
import type {
ReactFlowInstance,
Node,
Edge,
InternalNode,
ReactFlowState,
GeneralHelpers,
FitViewOptions,
} from '../types';
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 (
* <div>
* <button onClick={countNodes}>Update count</button>
* <p>There are {count} nodes in the flow.</p>
* </div>
* );
*}
*```
*/
export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(): ReactFlowInstance<
NodeType,
@@ -72,7 +101,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
setNodes((prevNodes) =>
prevNodes.map((node) => {
if (node.id === id) {
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as NodeType) : nodeUpdate;
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node) : nodeUpdate;
return options.replace && isNode(nextNode) ? (nextNode as NodeType) : { ...node, ...nextNode };
}
@@ -89,7 +118,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
setEdges((prevEdges) =>
prevEdges.map((edge) => {
if (edge.id === id) {
const nextEdge = typeof edgeUpdate === 'function' ? edgeUpdate(edge as EdgeType) : edgeUpdate;
const nextEdge = typeof edgeUpdate === 'function' ? edgeUpdate(edge) : edgeUpdate;
return options.replace && isEdge(nextEdge) ? (nextEdge as EdgeType) : { ...edge, ...nextEdge };
}
@@ -184,7 +213,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
return (nodes || store.getState().nodes).filter((n) => {
const internalNode = store.getState().nodeLookup.get(n.id);
if (internalNode && !isRect && (n.id === nodeOrRect!.id || !internalNode.internals.positionAbsolute)) {
if (internalNode && !isRect && (n.id === nodeOrRect.id || !internalNode.internals.positionAbsolute)) {
return false;
}
@@ -248,6 +277,17 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
.connectionLookup.get(`${nodeId}${type ? (handleId ? `-${type}-${handleId}` : `-${type}`) : ''}`)
?.values() ?? []
),
fitView: async (options: FitViewOptions<NodeType> | undefined) => {
// We either create a new Promise or reuse the existing one
// Even if fitView is called multiple times in a row, we only end up with a single Promise
const fitViewResolver = store.getState().fitViewResolver ?? Promise.withResolvers<boolean>();
// We schedule a fitView by setting fitViewQueued and triggering a setNodes
store.setState({ fitViewQueued: true, fitViewOptions: options, fitViewResolver });
batchContext.nodeQueue.push((nodes) => [...nodes]);
return fitViewResolver.promise;
},
};
}, []);
+1 -1
View File
@@ -16,7 +16,7 @@ export function useResizeHandler(domNode: MutableRefObject<HTMLDivElement | null
if (!domNode.current) {
return false;
}
const size = getDimensions(domNode.current!);
const size = getDimensions(domNode.current);
if (size.height === 0 || size.width === 0) {
store.getState().onError?.('004', errorMessages['error004']());
+29 -5
View File
@@ -9,16 +9,27 @@ 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
* @param equalityFn
* @returns The selected state slice
* @param selector - A selector function that returns a slice of the flow's internal state.
* Extracting or transforming just the state you need is a good practice to avoid unnecessary
* re-renders.
* @param equalityFn - A function to compare the previous and next value. This is incredibly useful
* for preventing unnecessary re-renders. Good sensible defaults are using `Object.is` or importing
* `zustand/shallow`, but you can be as granular as you like.
* @returns The selected state slice.
*
* @example
* const nodes = useStore((state: ReactFlowState<MyNodeType>) => 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<StateSlice = unknown>(
selector: (state: ReactFlowState) => StateSlice,
@@ -33,6 +44,19 @@ function useStore<StateSlice = unknown>(
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<NodeType extends Node = Node, EdgeType extends Edge = Edge>() {
const store = useContext(StoreContext) as UseBoundStoreWithEqualityFn<
StoreApi<ReactFlowState<NodeType, EdgeType>>
@@ -4,10 +4,49 @@ 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
* @returns Use this function to tell React Flow to update the internal state of one or more nodes
* that you have changed programmatically.
*
* @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) => (
* <Handle
* key={index}
* type="target"
* position="left"
* id={`handle-${index}`}
* />
* ))}
*
* <div>
* <button onClick={randomizeHandleCount}>Randomize handle count</button>
* <p>There are {handleCount} handles on this node.</p>
* </div>
* </>
* );
*}
*```
* @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();
+25 -2
View File
@@ -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
* @returns The current viewport.
*
* @example
*
*```jsx
*import { useViewport } from '@xyflow/react';
*
*export default function ViewportDisplay() {
* const { x, y, zoom } = useViewport();
*
* return (
* <div>
* <p>
* The viewport is currently at ({x}, {y}) and zoomed to {zoom}.
* </p>
* </div>
* );
*}
*```
*
* @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);
+9 -29
View File
@@ -2,11 +2,9 @@ import { useMemo } from 'react';
import {
pointToRendererPoint,
getViewportForBounds,
getFitViewNodes,
fitView,
type XYPosition,
rendererPointToPoint,
getDimensions,
SnapGrid,
} from '@xyflow/system';
import { useStoreApi } from '../hooks/useStore';
@@ -64,28 +62,6 @@ const useViewportHelper = (): ViewportHelperFunctions => {
const [x, y, zoom] = store.getState().transform;
return { x, y, zoom };
},
fitView: (options) => {
const { nodeLookup, minZoom, maxZoom, panZoom, domNode } = store.getState();
if (!panZoom || !domNode) {
return Promise.resolve(false);
}
const fitViewNodes = getFitViewNodes(nodeLookup, options);
const { width, height } = getDimensions(domNode);
return fitView(
{
nodes: fitViewNodes,
width,
height,
minZoom,
maxZoom,
panZoom,
},
options
);
},
setCenter: async (x, y, options) => {
const { width, height, maxZoom, panZoom } = store.getState();
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom;
@@ -119,21 +95,25 @@ const useViewportHelper = (): ViewportHelperFunctions => {
return Promise.resolve(true);
},
screenToFlowPosition: (clientPosition: XYPosition, options: { snapToGrid: boolean } = { snapToGrid: true }) => {
const { transform, snapGrid, domNode } = store.getState();
screenToFlowPosition: (
clientPosition: XYPosition,
options: { snapToGrid?: boolean; snapGrid?: SnapGrid } = {}
) => {
const { transform, snapGrid, snapToGrid, domNode } = store.getState();
if (!domNode) {
return clientPosition;
}
const { x: domX, y: domY } = domNode.getBoundingClientRect();
const correctedPosition = {
x: clientPosition.x - domX,
y: clientPosition.y - domY,
};
const _snapGrid = options.snapGrid ?? snapGrid;
const _snapToGrid = options.snapToGrid ?? snapToGrid;
return pointToRendererPoint(correctedPosition, transform, options.snapToGrid, snapGrid);
return pointToRendererPoint(correctedPosition, transform, _snapToGrid, _snapGrid);
},
flowToScreenPosition: (flowPosition: XYPosition) => {
const { transform, domNode } = store.getState();
@@ -8,8 +8,8 @@ import type { Node, ReactFlowState } from '../types';
const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
return onlyRenderVisible
? getNodesInside<Node>(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());
};