Merge pull request #5703 from xyflow/fix/use-nodes-data-type

Improve return type of useNodesData
This commit is contained in:
Moritz Klack
2026-02-16 20:57:22 +01:00
committed by GitHub
7 changed files with 33 additions and 18 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@xyflow/react': patch
'@xyflow/svelte': patch
'@xyflow/system': patch
---
Improve return type of useNodesData. Now you can narrow down the data type by checking the node type.
@@ -1,7 +1,8 @@
import { memo, useState } from 'react'; import { memo, useState } from 'react';
import { Position, NodeProps, Handle, useReactFlow } from '@xyflow/react'; import { Position, NodeProps, Handle, useReactFlow } from '@xyflow/react';
import type { TextNode } from '.';
function TextNode({ id, data }: NodeProps) { function TextNode({ id, data }: NodeProps<TextNode>) {
const { updateNodeData } = useReactFlow(); const { updateNodeData } = useReactFlow();
const [text, setText] = useState(data.text); const [text, setText] = useState(data.text);
const updateText = (text: string) => { const updateText = (text: string) => {
@@ -1,18 +1,18 @@
import { memo, useEffect } from 'react'; import { memo, useEffect } from 'react';
import { Position, NodeProps, useReactFlow, Handle, useNodeConnections, useNodesData } from '@xyflow/react'; import { Position, NodeProps, useReactFlow, Handle, useNodeConnections, useNodesData } from '@xyflow/react';
import { isTextNode, type TextNode, type MyNode } from '.'; import { isTextNode, type TextNode, type MyNode, type UppercaseNode } from '.';
function UppercaseNode({ id }: NodeProps) { function UppercaseNode({ id }: NodeProps<UppercaseNode>) {
const { updateNodeData } = useReactFlow(); const { updateNodeData } = useReactFlow();
const connections = useNodeConnections({ const connections = useNodeConnections({
handleType: 'target', handleType: 'target',
}); });
const nodesData = useNodesData<MyNode>(connections[0]?.source); const nodesData = useNodesData<MyNode>(connections[0]?.source);
const textNode = isTextNode(nodesData) ? nodesData : null;
useEffect(() => { useEffect(() => {
updateNodeData(id, { text: textNode?.data.text.toUpperCase() }); const text = nodesData?.type === 'text' ? nodesData?.data.text.toUpperCase() : undefined;
}, [textNode]); updateNodeData(id, { text });
}, [nodesData]);
return ( return (
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}> <div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
@@ -8,22 +8,21 @@
type Node, type Node,
type NodeProps type NodeProps
} from '@xyflow/svelte'; } from '@xyflow/svelte';
import { isTextNode, type MyNode } from './+page.svelte'; import { type MyNode } from './+page.svelte';
let { id }: NodeProps<Node<{ text: string }>> = $props(); let { id }: NodeProps<Node<{ text: string }>> = $props();
const { updateNodeData } = useSvelteFlow(); const { updateNodeData } = useSvelteFlow();
const connections = useNodeConnections({ const connections = useNodeConnections({
id: id,
handleType: 'target' handleType: 'target'
}); });
let nodeData = $derived(useNodesData<MyNode>(connections.current[0]?.source)); let nodesData = $derived(useNodesData<MyNode>(connections.current[0]?.source));
let textNodeData = $derived(isTextNode(nodeData.current) ? nodeData.current.data.text : null);
$effect.pre(() => { $effect.pre(() => {
const input = textNodeData?.toUpperCase() ?? ''; const text =
updateNodeData(id, { text: input }); nodesData.current?.type === 'text' ? nodesData.current?.data.text.toUpperCase() : undefined;
updateNodeData(id, { text });
}); });
</script> </script>
+3 -3
View File
@@ -1,5 +1,5 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { shallowNodeData } from '@xyflow/system'; import { DistributivePick, shallowNodeData } from '@xyflow/system';
import { useStore } from '../hooks/useStore'; import { useStore } from '../hooks/useStore';
import type { Node } from '../types'; import type { Node } from '../types';
@@ -25,11 +25,11 @@ import type { Node } from '../types';
export function useNodesData<NodeType extends Node = Node>( export function useNodesData<NodeType extends Node = Node>(
/** The id of the node to get the data from. */ /** The id of the node to get the data from. */
nodeId: string nodeId: string
): Pick<NodeType, 'id' | 'type' | 'data'> | null; ): DistributivePick<NodeType, 'id' | 'type' | 'data'> | null;
export function useNodesData<NodeType extends Node = Node>( export function useNodesData<NodeType extends Node = Node>(
/** The ids of the nodes to get the data from. */ /** The ids of the nodes to get the data from. */
nodeIds: string[] nodeIds: string[]
): Pick<NodeType, 'id' | 'type' | 'data'>[]; ): DistributivePick<NodeType, 'id' | 'type' | 'data'>[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useNodesData(nodeIds: any): any { export function useNodesData(nodeIds: any): any {
const nodesData = useStore( const nodesData = useStore(
@@ -1,4 +1,4 @@
import { shallowNodeData } from '@xyflow/system'; import { shallowNodeData, type DistributivePick } from '@xyflow/system';
import type { Node } from '$lib/types'; import type { Node } from '$lib/types';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
@@ -12,10 +12,10 @@ import { useStore } from '$lib/store';
*/ */
export function useNodesData<NodeType extends Node = Node>( export function useNodesData<NodeType extends Node = Node>(
nodeId: string nodeId: string
): { current: Pick<NodeType, 'id' | 'data' | 'type'> | null }; ): { current: DistributivePick<NodeType, 'id' | 'data' | 'type'> | null };
export function useNodesData<NodeType extends Node = Node>( export function useNodesData<NodeType extends Node = Node>(
nodeIds: string[] nodeIds: string[]
): { current: Pick<NodeType, 'id' | 'data' | 'type'>[] }; ): { current: DistributivePick<NodeType, 'id' | 'data' | 'type'>[] };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useNodesData(nodeIds: any): any { export function useNodesData(nodeIds: any): any {
const { nodes, nodeLookup } = $derived(useStore()); const { nodes, nodeLookup } = $derived(useStore());
+7
View File
@@ -56,3 +56,10 @@ export type Transform = [number, number, number];
* to represent an unbounded extent. * to represent an unbounded extent.
*/ */
export type CoordinateExtent = [[number, number], [number, number]]; export type CoordinateExtent = [[number, number], [number, number]];
/**
* Using Pick with a union type (e.g. `NodeType`) will merge every property type along all union members.
* See https://github.com/microsoft/TypeScript/issues/28339#issuecomment-463577347
* Note: Currently you are able to Pick properties that are not in the type without error.
*/
export type DistributivePick<T, K extends string> = T extends unknown ? { [P in Extract<keyof T, K>]: T[P] } : never;