From a7b6789ce03c6e3c5ab9ccd2ca4b9b37028abf9b Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 25 Apr 2024 12:57:24 +0200 Subject: [PATCH 1/6] fix(react): handle setNodes batching in provider #4147 --- .../src/components/BatchProvider/index.tsx | 80 +++++++++++ .../src/components/BatchProvider/types.ts | 7 + .../src/components/BatchProvider/useQueue.ts | 44 ++++++ .../src/components/BatchProvider/utils.ts | 16 +++ .../components/ReactFlowProvider/index.tsx | 7 +- packages/react/src/hooks/useReactFlow.ts | 134 +++++------------- packages/react/src/utils/changes.ts | 3 +- 7 files changed, 187 insertions(+), 104 deletions(-) create mode 100644 packages/react/src/components/BatchProvider/index.tsx create mode 100644 packages/react/src/components/BatchProvider/types.ts create mode 100644 packages/react/src/components/BatchProvider/useQueue.ts create mode 100644 packages/react/src/components/BatchProvider/utils.ts diff --git a/packages/react/src/components/BatchProvider/index.tsx b/packages/react/src/components/BatchProvider/index.tsx new file mode 100644 index 00000000..a3acec3e --- /dev/null +++ b/packages/react/src/components/BatchProvider/index.tsx @@ -0,0 +1,80 @@ +import { createContext, ReactNode, useCallback, useContext, useMemo } from 'react'; +import { EdgeChange, NodeChange } from '@xyflow/system'; + +import { useStoreApi } from '../../hooks/useStore'; +import { getElementsDiffChanges } from '../../utils'; +import { Queue, QueueItem } from './types'; +import type { Edge, Node } from '../../types'; +import { useQueue } from './useQueue'; + +const BatchContext = createContext<{ + nodeQueue: Queue; + edgeQueue: Queue; +} | null>(null); + +export function BatchProvider({ + children, +}: { + children: ReactNode; +}) { + const store = useStoreApi(); + + const nodeQueueHandler = useCallback((queueItems: QueueItem[]) => { + 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. + let next = nodes as NodeType[]; + for (const payload of queueItems) { + next = typeof payload === 'function' ? payload(next) : payload; + } + + if (hasDefaultNodes) { + setNodes(next); + } else if (onNodesChange) { + onNodesChange( + getElementsDiffChanges({ + items: next, + lookup: nodeLookup, + }) as NodeChange[] + ); + } + }, []); + const nodeQueue = useQueue(nodeQueueHandler); + + const edgeQueueHandler = useCallback((queueItems: QueueItem[]) => { + const { edges = [], setEdges, hasDefaultEdges, onEdgesChange, edgeLookup } = store.getState(); + + let next = edges as EdgeType[]; + for (const payload of queueItems) { + next = typeof payload === 'function' ? payload(next) : payload; + } + + if (hasDefaultEdges) { + setEdges(next); + } else if (onEdgesChange) { + onEdgesChange( + getElementsDiffChanges({ + items: next, + lookup: edgeLookup, + }) as EdgeChange[] + ); + } + }, []); + const edgeQueue = useQueue(edgeQueueHandler); + + const value = useMemo(() => ({ nodeQueue, edgeQueue }), []); + + return {children}; +} + +export function useBatchContext() { + const batchContext = useContext(BatchContext); + + if (!batchContext) { + throw new Error('useBatchContext must be used within a BatchProvider'); + } + + return batchContext; +} diff --git a/packages/react/src/components/BatchProvider/types.ts b/packages/react/src/components/BatchProvider/types.ts new file mode 100644 index 00000000..53d7193b --- /dev/null +++ b/packages/react/src/components/BatchProvider/types.ts @@ -0,0 +1,7 @@ +export type QueueItem = T[] | ((items: T[]) => T[]); + +export type Queue = { + get: () => QueueItem[]; + reset: () => void; + push: (item: QueueItem) => void; +}; diff --git a/packages/react/src/components/BatchProvider/useQueue.ts b/packages/react/src/components/BatchProvider/useQueue.ts new file mode 100644 index 00000000..a9fa64b8 --- /dev/null +++ b/packages/react/src/components/BatchProvider/useQueue.ts @@ -0,0 +1,44 @@ +import { useState } from 'react'; +import { createQueue } from './utils'; +import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect'; +import { QueueItem } from './types'; + +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 flip this bit of state to `true` any time we + // mutate the queue and then flip it back to `false` after flushing the queue. + const [shouldFlush, setShouldFlush] = useState(false); + + // A reference of all the batched updates to process before the next render. We + // want a mutable reference here so multiple synchronous calls to `setNodes` etc + // can be batched together. + const [queue] = useState(() => createQueue(() => setShouldFlush(true))); + + // 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(() => { + // Because we need to flip the state back to false after flushing, this should + // trigger the hook again (!). If the hook is being run again we know that any + // updates should have been processed by now and we can safely clear the queue + // and bail early. + if (!shouldFlush) { + queue.reset(); + return; + } + + const queueItems = queue.get(); + + if (queueItems.length) { + runQueue?.(queueItems); + + queue.reset(); + } + + // Beacuse we're using reactive state to trigger this effect, we need to flip + // it back to false. + setShouldFlush(false); + }, [shouldFlush]); + + return queue; +} diff --git a/packages/react/src/components/BatchProvider/utils.ts b/packages/react/src/components/BatchProvider/utils.ts new file mode 100644 index 00000000..50026c87 --- /dev/null +++ b/packages/react/src/components/BatchProvider/utils.ts @@ -0,0 +1,16 @@ +import type { Queue, QueueItem } from './types'; + +export function createQueue(cb: () => void): Queue { + let queue: QueueItem[] = []; + + return { + get: () => queue, + reset: () => { + queue = []; + }, + push: (item) => { + queue.push(item); + cb(); + }, + }; +} diff --git a/packages/react/src/components/ReactFlowProvider/index.tsx b/packages/react/src/components/ReactFlowProvider/index.tsx index 33d3d4c4..f04ebebd 100644 --- a/packages/react/src/components/ReactFlowProvider/index.tsx +++ b/packages/react/src/components/ReactFlowProvider/index.tsx @@ -2,6 +2,7 @@ import { useState, type ReactNode } from 'react'; import { Provider } from '../../contexts/StoreContext'; import { createStore } from '../../store'; +import { BatchProvider } from '../BatchProvider'; import type { Node, Edge } from '../../types'; export type ReactFlowProviderProps = { @@ -37,5 +38,9 @@ export function ReactFlowProvider({ }) ); - return {children}; + return ( + + {children} + + ); } diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 56c9ae5d..284aa3ac 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { evaluateAbsolutePosition, getElementsToRemove, @@ -10,9 +10,9 @@ import { import useViewportHelper from './useViewportHelper'; import { useStoreApi } from './useStore'; +import { useBatchContext } from '../components/BatchProvider'; +import { isNode } from '../utils'; import type { ReactFlowInstance, Instance, Node, Edge, InternalNode } from '../types'; -import { getElementsDiffChanges, isNode } from '../utils'; -import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; /** * Hook for accessing the ReactFlow instance. @@ -26,6 +26,7 @@ export function useReactFlow { const viewportHelper = useViewportHelper(); const store = useStoreApi(); + const batchContext = useBatchContext(); const getNodes = useCallback>( () => store.getState().nodes.map((n) => ({ ...n })) as NodeType[], @@ -49,110 +50,39 @@ export function useReactFlow>((id) => store.getState().edgeLookup.get(id) as EdgeType, []); - type SetElementsQueue = { - nodes: (NodeType[] | ((nodes: NodeType[]) => NodeType[]))[]; - edges: (EdgeType[] | ((edges: EdgeType[]) => EdgeType[]))[]; - }; + const setNodes = useCallback>( + (payload) => { + batchContext?.nodeQueue.push(payload as NodeType[]); + }, + [batchContext] + ); - // A reference of all the batched updates to process before the next render. We - // want a mutable reference here so multiple synchronous calls to `setNodes` etc - // can be batched together. - const setElementsQueue = useRef({ nodes: [], edges: [] }); - // Because we're using a ref above, we need some way to let React know when to - // actually process the queue. We flip this bit of state to `true` any time we - // mutate the queue and then flip it back to `false` after flushing the queue. - const [shouldFlushQueue, setShouldFlushQueue] = useState(false); + const setEdges = useCallback>( + (payload) => { + batchContext?.edgeQueue.push(payload as EdgeType[]); + }, + [batchContext] + ); - // 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(() => { - // Because we need to flip the state back to false after flushing, this should - // trigger the hook again (!). If the hook is being run again we know that any - // updates should have been processed by now and we can safely clear the queue - // and bail early. - if (!shouldFlushQueue) { - setElementsQueue.current = { nodes: [], edges: [] }; - return; - } + const addNodes = useCallback>( + (payload) => { + const newNodes = Array.isArray(payload) ? payload : [payload]; - if (setElementsQueue.current.nodes.length) { - const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState(); + // Queueing a functional update means that we won't worry about other calls + // to `setNodes` that might happen elsewhere. + batchContext?.nodeQueue.push((nodes) => [...nodes, ...newNodes]); + }, + [batchContext] + ); - // 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 setElementsQueue.current.nodes) { - next = typeof payload === 'function' ? payload(next) : payload; - } + const addEdges = useCallback>( + (payload) => { + const newEdges = Array.isArray(payload) ? payload : [payload]; - if (hasDefaultNodes) { - setNodes(next); - } else if (onNodesChange) { - onNodesChange( - getElementsDiffChanges({ - items: next, - lookup: nodeLookup, - }) - ); - } - - setElementsQueue.current.nodes = []; - } - - if (setElementsQueue.current.edges.length) { - const { edges = [], setEdges, hasDefaultEdges, onEdgesChange, edgeLookup } = store.getState(); - - let next = edges as EdgeType[]; - for (const payload of setElementsQueue.current.edges) { - next = typeof payload === 'function' ? payload(next) : payload; - } - - if (hasDefaultEdges) { - setEdges(next); - } else if (onEdgesChange) { - onEdgesChange( - getElementsDiffChanges({ - items: next, - lookup: edgeLookup, - }) - ); - } - - setElementsQueue.current.edges = []; - } - - // Beacuse we're using reactive state to trigger this effect, we need to flip - // it back to false. - setShouldFlushQueue(false); - }, [shouldFlushQueue]); - - const setNodes = useCallback>((payload) => { - setElementsQueue.current.nodes.push(payload); - setShouldFlushQueue(true); - }, []); - - const setEdges = useCallback>((payload) => { - setElementsQueue.current.edges.push(payload); - setShouldFlushQueue(true); - }, []); - - const addNodes = useCallback>((payload) => { - const newNodes = Array.isArray(payload) ? payload : [payload]; - - // Queueing a functional update means that we won't worry about other calls - // to `setNodes` that might happen elsewhere. - setElementsQueue.current.nodes.push((nodes) => [...nodes, ...newNodes]); - setShouldFlushQueue(true); - }, []); - - const addEdges = useCallback>((payload) => { - const newEdges = Array.isArray(payload) ? payload : [payload]; - - setElementsQueue.current.edges.push((edges) => [...edges, ...newEdges]); - setShouldFlushQueue(true); - }, []); + batchContext?.edgeQueue.push((edges) => [...edges, ...newEdges]); + }, + [batchContext] + ); const toObject = useCallback>(() => { const { nodes = [], edges = [], transform } = store.getState(); diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index 294af3e1..cdc83088 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -236,7 +236,8 @@ export function getElementsDiffChanges({ const itemsLookup = new Map(items.map((item) => [item.id, item])); for (const item of items) { - const storeItem = lookup.get(item.id); + const lookupItem = lookup.get(item.id); + const storeItem = lookupItem?.internals?.userNode ?? lookupItem; if (storeItem !== undefined && storeItem !== item) { changes.push({ id: item.id, item: item, type: 'replace' }); From 95fd22d25bc7db60fd741993fc134c9dfac6331e Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 25 Apr 2024 15:08:10 +0200 Subject: [PATCH 2/6] chore(BatchProvider): cleanup --- .../react/src/examples/UseNodesData/index.tsx | 35 ++++++++++++--- .../src/components/BatchProvider/index.tsx | 12 ++++- .../src/components/BatchProvider/useQueue.ts | 35 ++++++++++++--- packages/react/src/hooks/useReactFlow.ts | 44 ++++++------------- 4 files changed, 81 insertions(+), 45 deletions(-) diff --git a/examples/react/src/examples/UseNodesData/index.tsx b/examples/react/src/examples/UseNodesData/index.tsx index dc0b43dd..dabc1d4d 100644 --- a/examples/react/src/examples/UseNodesData/index.tsx +++ b/examples/react/src/examples/UseNodesData/index.tsx @@ -45,6 +45,12 @@ const initNodes: MyNode[] = [ data: { text: '' }, position: { x: 100, y: 0 }, }, + { + id: '1b', + type: 'uppercase', + data: { text: '' }, + position: { x: 100, y: -100 }, + }, { id: '2', type: 'text', @@ -53,9 +59,14 @@ const initNodes: MyNode[] = [ }, position: { x: 0, y: 100 }, }, - { - id: '3', + id: '3a', + type: 'result', + data: {}, + position: { x: 300, y: -75 }, + }, + { + id: '3b', type: 'result', data: {}, position: { x: 300, y: 50 }, @@ -69,14 +80,24 @@ const initEdges: Edge[] = [ target: '1a', }, { - id: 'e1a-3', - source: '1a', - target: '3', + id: 'e1a-3a', + source: '1b', + target: '3a', }, { - id: 'e2-3', + id: 'e1-1b', + source: '1', + target: '1b', + }, + { + id: 'e1a-3b', + source: '1a', + target: '3b', + }, + { + id: 'e2-3b', source: '2', - target: '3', + target: '3b', }, ]; diff --git a/packages/react/src/components/BatchProvider/index.tsx b/packages/react/src/components/BatchProvider/index.tsx index a3acec3e..1dbab718 100644 --- a/packages/react/src/components/BatchProvider/index.tsx +++ b/packages/react/src/components/BatchProvider/index.tsx @@ -8,10 +8,18 @@ import type { Edge, Node } from '../../types'; import { useQueue } from './useQueue'; const BatchContext = createContext<{ - nodeQueue: Queue; - edgeQueue: Queue; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + nodeQueue: Queue; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + edgeQueue: Queue; } | null>(null); +/** + * This is a context provider that holds and processes the node and edge update queues + * that are needed to handle setNodes, addNodes, setEdges and addEdges. + * + * @internal + */ export function BatchProvider({ children, }: { diff --git a/packages/react/src/components/BatchProvider/useQueue.ts b/packages/react/src/components/BatchProvider/useQueue.ts index a9fa64b8..06d9e235 100644 --- a/packages/react/src/components/BatchProvider/useQueue.ts +++ b/packages/react/src/components/BatchProvider/useQueue.ts @@ -1,8 +1,16 @@ import { useState } from 'react'; -import { createQueue } from './utils'; -import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect'; -import { QueueItem } from './types'; +import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect'; +import { Queue, QueueItem } from './types'; + +/** + * This hook returns a queue that can be used to batch updates. + * + * @param runQueue - a function that gets called when the queue is flushed + * @internal + * + * @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 flip this bit of state to `true` any time we @@ -10,8 +18,8 @@ export function useQueue(runQueue: (items: QueueItem[]) => void) { const [shouldFlush, setShouldFlush] = useState(false); // A reference of all the batched updates to process before the next render. We - // want a mutable reference here so multiple synchronous calls to `setNodes` etc - // can be batched together. + // want a reference here so multiple synchronous calls to `setNodes` etc can be + // batched together. const [queue] = useState(() => createQueue(() => setShouldFlush(true))); // Layout effects are guaranteed to run before the next render which means we @@ -30,7 +38,7 @@ export function useQueue(runQueue: (items: QueueItem[]) => void) { const queueItems = queue.get(); if (queueItems.length) { - runQueue?.(queueItems); + runQueue(queueItems); queue.reset(); } @@ -42,3 +50,18 @@ export function useQueue(runQueue: (items: QueueItem[]) => void) { return queue; } + +function createQueue(cb: () => void): Queue { + let queue: QueueItem[] = []; + + return { + get: () => queue, + reset: () => { + queue = []; + }, + push: (item) => { + queue.push(item); + cb(); + }, + }; +} diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 284aa3ac..4ac0e50c 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -50,39 +50,23 @@ export function useReactFlow>((id) => store.getState().edgeLookup.get(id) as EdgeType, []); - const setNodes = useCallback>( - (payload) => { - batchContext?.nodeQueue.push(payload as NodeType[]); - }, - [batchContext] - ); + const setNodes = useCallback>((payload) => { + batchContext.nodeQueue.push(payload as NodeType[]); + }, []); - const setEdges = useCallback>( - (payload) => { - batchContext?.edgeQueue.push(payload as EdgeType[]); - }, - [batchContext] - ); + const setEdges = useCallback>((payload) => { + batchContext.edgeQueue.push(payload as EdgeType[]); + }, []); - const addNodes = useCallback>( - (payload) => { - const newNodes = Array.isArray(payload) ? payload : [payload]; + const addNodes = useCallback>((payload) => { + const newNodes = Array.isArray(payload) ? payload : [payload]; + batchContext.nodeQueue.push((nodes) => [...nodes, ...newNodes]); + }, []); - // Queueing a functional update means that we won't worry about other calls - // to `setNodes` that might happen elsewhere. - batchContext?.nodeQueue.push((nodes) => [...nodes, ...newNodes]); - }, - [batchContext] - ); - - const addEdges = useCallback>( - (payload) => { - const newEdges = Array.isArray(payload) ? payload : [payload]; - - batchContext?.edgeQueue.push((edges) => [...edges, ...newEdges]); - }, - [batchContext] - ); + const addEdges = useCallback>((payload) => { + const newEdges = Array.isArray(payload) ? payload : [payload]; + batchContext.edgeQueue.push((edges) => [...edges, ...newEdges]); + }, []); const toObject = useCallback>(() => { const { nodes = [], edges = [], transform } = store.getState(); From 3a23a594cfa2638a03ab0f50b9e903e0d430b771 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 25 Apr 2024 16:41:09 +0200 Subject: [PATCH 3/6] chore(BatchProvider): cleanup --- .../react/src/components/BatchProvider/utils.ts | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 packages/react/src/components/BatchProvider/utils.ts diff --git a/packages/react/src/components/BatchProvider/utils.ts b/packages/react/src/components/BatchProvider/utils.ts deleted file mode 100644 index 50026c87..00000000 --- a/packages/react/src/components/BatchProvider/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Queue, QueueItem } from './types'; - -export function createQueue(cb: () => void): Queue { - let queue: QueueItem[] = []; - - return { - get: () => queue, - reset: () => { - queue = []; - }, - push: (item) => { - queue.push(item); - cb(); - }, - }; -} From 0b684a7fe97209186294d394a05fedf4387d24b6 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 25 Apr 2024 16:47:49 +0200 Subject: [PATCH 4/6] chore(changelog): update --- packages/react/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 43fb4636..06402425 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,11 @@ # @xyflow/react +## 12.0.0-next.16 + +## Patch changes + +- fix batching for `setNodes`, `updateNode`, `updateNodeData` etc. + ## 12.0.0-next.15 ## Patch changes From 972da582e96e20e0562d5f645d19a8e43630cdc0 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 25 Apr 2024 17:02:57 +0200 Subject: [PATCH 5/6] fix(react): useNodesInitialized true when nodes measured closes #4202 --- packages/react/CHANGELOG.md | 1 + packages/react/src/hooks/useNodesInitialized.ts | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 06402425..6685ad5b 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -5,6 +5,7 @@ ## Patch changes - fix batching for `setNodes`, `updateNode`, `updateNodeData` etc. +- fix `useNodesInitialized` ## 12.0.0-next.15 diff --git a/packages/react/src/hooks/useNodesInitialized.ts b/packages/react/src/hooks/useNodesInitialized.ts index 346e56d9..98bfa6b7 100644 --- a/packages/react/src/hooks/useNodesInitialized.ts +++ b/packages/react/src/hooks/useNodesInitialized.ts @@ -1,5 +1,6 @@ import { useStore } from './useStore'; import type { ReactFlowState } from '../types'; +import { nodeHasDimensions } from '@xyflow/system'; export type UseNodesInitializedOptions = { includeHiddenNodes?: boolean; @@ -10,9 +11,9 @@ const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) => return false; } - for (const [, node] of s.nodeLookup) { - if (options.includeHiddenNodes || !node.hidden) { - if (node.internals.handleBounds === undefined) { + for (const [, { hidden, internals }] of s.nodeLookup) { + if (options.includeHiddenNodes || !hidden) { + if (internals.handleBounds === undefined || !nodeHasDimensions(internals.userNode)) { return false; } } From 8c784c2035c897e2b33ef0a34bc70d8cea130fdd Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 25 Apr 2024 17:05:51 +0200 Subject: [PATCH 6/6] chore(react): bump --- packages/react/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/package.json b/packages/react/package.json index 54a6dfa5..3b99d721 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/react", - "version": "12.0.0-next.15", + "version": "12.0.0-next.16", "description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.", "keywords": [ "react",