Skip to Content
教程教程思维导图应用

2023/01/10

使用 React Flow 构建思维导图应用

Moritz Klack
Co-Founder

在本教程中,你将学习如何使用 React Flow 创建一个简单的思维导图工具,可用于头脑风暴、组织想法或以可视化方式绘制思维。为了构建这个应用,我们将使用状态管理、自定义节点和边等功能。

🌐 In this tutorial, you will learn to create a simple mind map tool with React Flow that can be used for brainstorming, organizing an idea, or mapping your thoughts in a visual way. To build this app, we’ll be using state management, custom nodes and edges, and more.

是演示时间!

在我们开始动手之前,我想向你展示本教程结束时我们将拥有的思维导图工具:

🌐 Before we get our hands dirty, I want to show you the mind-mapping tool we’ll have by the end of this tutorial:

如果你想冒险直接深入代码,你可以在 Github  上找到源代码。

🌐 If you’d like to live dangerously and dive right into the code, you can find the source code on Github .

入门

要完成本教程,你需要具备一些 React React Flow 的知识(嗨,那就是我们! 它是一个用于构建基于节点的用户界面的开源库,如工作流工具、ETL 管道以及 更多。)

我们将使用 Vite  来开发我们的应用,但你也可以使用 Create React App  或任何你喜欢的工具。要使用 Vite 创建一个新的 React 应用,你需要执行以下操作:

🌐 We’ll be using Vite  to develop our app, but you can also use Create React App  or any other tool you like. To scaffold a new React app with Vite you need to do:

npm create vite@latest reactflow-mind-map -- --template react

如果你想使用 Typescript:

🌐 if you would like to use Typescript:

npm create vite@latest reactflow-mind-map -- --template react-ts

初始设置后,你需要安装一些软件包:

🌐 After the initial setup, you need to install some packages:

npm install reactflow zustand classcat nanoid

我们正在使用 Zustand  来管理我们应用的状态。它有点像 Redux,但要小得多,而且需要编写的样板代码更少。React Flow 也使用 Zustand,所以安装它不会额外增加成本。(在本教程中我们使用的是 Typescript,但你也可以使用普通的 Javascript。)

🌐 We are using Zustand  for managing the state of our application. It’s a bit like Redux but way smaller and there’s less boilerplate code to write. React Flow also uses Zustand, so the installation comes with no additional cost. (For this tutorial we are using Typescript but you can also use plain Javascript.)

为了保持简单,我们将所有代码放在 src/App 文件夹中。为此,你需要创建 src/App 文件夹并添加一个包含以下内容的索引文件:

🌐 To keep it simple we are putting all of our code in the src/App folder. For this you need to create the src/App folder and add an index file with the following content:

src/App/index.tsx

import { ReactFlow, Controls, Panel } from '@xyflow/react'; // we have to import the React Flow styles for it to work import '@xyflow/react/dist/style.css'; function Flow() { return ( <ReactFlow> <Controls showInteractive={false} /> <Panel position="top-left">React Flow Mind Map</Panel> </ReactFlow> ); } export default Flow;

这将是我们用于渲染思维导图的主要组件。目前还没有节点或边,但我们添加了 React Flow Controls 组件和一个 Panel 来显示我们应用的标题。

🌐 This will be our main component for rendering the mind map. There are no nodes or edges yet, but we added the React Flow Controls component and a Panel to display the title of our app.

为了能够使用 React Flow 钩子,我们需要在 main.tsx(Vite 的入口文件)中用 ReactFlowProvider 组件封装应用。我们还导入了新创建的 App/index.tsx 并将其渲染在 ReactFlowProvider. 内。你的主文件应该看起来像这样:

🌐 To be able to use React Flow hooks, we need to wrap the application with the ReactFlowProvider component in our main.tsx (entry file for vite). We are also importing the newly created App/index.tsx and render it inside the ReactFlowProvider. Your main file should look like this:

src/main.tsx

import React from 'react'; import ReactDOM from 'react-dom/client'; import { ReactFlowProvider } from '@xyflow/react'; import App from './App'; import './index.css'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( <React.StrictMode> <ReactFlowProvider> <App /> </ReactFlowProvider> </React.StrictMode>, );

React Flow 组件的父容器需要宽度和高度才能正常工作。我们的应用是全屏应用,所以我们将这些规则添加到 index.css 文件中:

🌐 The parent container of the React Flow component needs a width and a height to work properly. Our app is a fullscreen app, so we add these rules to the index.css file:

src/index.css

body { margin: 0; } html, body, #root { height: 100%; }

我们正在将应用的所有样式添加到 index.css 文件中(你也可以使用 Tailwind)。现在你可以使用 npm run dev 启动开发服务器,你应该会看到以下内容:

🌐 We are adding all styles of our app to the index.css file (you could also use Tailwind). Now you can start the development server with npm run dev and you should see the following:

一个用于存储节点和边的商店

如上所述,我们正在使用 Zustand 进行状态管理。为此,我们在 src/App 文件夹中创建一个新的文件,名为 store.ts

🌐 As mentioned above, we are using Zustand for state management. For this, we create a new file in our src/App folder called store.ts:

src/App/store.ts

import { Edge, EdgeChange, Node, NodeChange, OnNodesChange, OnEdgesChange, applyNodeChanges, applyEdgeChanges, } from '@xyflow/react'; import { createWithEqualityFn } from 'zustand/traditional'; export type RFState = { nodes: Node[]; edges: Edge[]; onNodesChange: OnNodesChange; onEdgesChange: OnEdgesChange; }; const useStore = createWithEqualityFn<RFState>((set, get) => ({ nodes: [ { id: 'root', type: 'mindmap', data: { label: 'React Flow Mind Map' }, position: { x: 0, y: 0 }, }, ], edges: [], onNodesChange: (changes: NodeChange[]) => { set({ nodes: applyNodeChanges(changes, get().nodes), }); }, onEdgesChange: (changes: EdgeChange[]) => { set({ edges: applyEdgeChanges(changes, get().edges), }); }, })); export default useStore;

看起来像很多代码,但主要是类型 存储器跟踪节点和边,并处理更改事件。当用户拖动节点时,React Flow 会触发更改事件,然后存储器应用这些更改,更新后的节点被渲染。(你可以在我们的状态管理库指南中了解更多。)

正如你所看到的,我们从一个位于 { x: 0, y: 0 } 的初始节点开始,类型为 ‘mindmap’。要将商店与我们的应用连接,我们使用 useStore 钩子:

🌐 As you can see we start with one initial node placed at { x: 0, y: 0 } of type ‘mindmap’. To connect the store with our app, we use the useStore hook:

src/App/index.tsx

import { ReactFlow, Controls, Panel, NodeOrigin } from '@xyflow/react'; import { shallow } from 'zustand/shallow'; import useStore, { RFState } from './store'; // we have to import the React Flow styles for it to work import '@xyflow/react/dist/style.css'; const selector = (state: RFState) => ({ nodes: state.nodes, edges: state.edges, onNodesChange: state.onNodesChange, onEdgesChange: state.onEdgesChange, }); // this places the node origin in the center of a node const nodeOrigin: NodeOrigin = [0.5, 0.5]; function Flow() { // whenever you use multiple values, you should use shallow to make sure the component only re-renders when one of the values changes const { nodes, edges, onNodesChange, onEdgesChange } = useStore(selector, shallow); return ( <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} nodeOrigin={nodeOrigin} fitView > <Controls showInteractive={false} /> <Panel position="top-left">React Flow Mind Map</Panel> </ReactFlow> ); } export default Flow;

我们从存储中访问节点、边和变更处理器,并将它们传递给 React Flow 组件。我们还使用 fitView 属性来确保初始节点在视图中居中,并将节点原点设置为 [0.5, 0.5] 以将原点设置在节点的中心。完成这些后,你的应用应该如下所示:

🌐 We access the nodes, edges and change handlers from the store and pass them to the React Flow component. We also use the fitView prop to make sure that the initial node is centered in the view and set the node origin to [0.5, 0.5] to set the origin to the center of a node. After this, your app should look like this:

你可以移动节点并进行缩放,我们正在取得进展 现在让我们添加一些更多功能。

自定义节点和边

我们想为我们的节点使用一种名为“mindmap”的自定义类型。我们需要为此添加一个新组件。让我们在 src/App 下创建一个名为 MindMapNode 的新文件夹,并在其中创建一个索引文件,内容如下:

🌐 We want to use a custom type called ‘mindmap’ for our nodes. We need to add a new component for this. Let’s create a new folder called MindMapNode with an index file under src/App with the following content:

src/App/MindMapNode/index.tsx

import { Handle, NodeProps, Position } from '@xyflow/react'; export type NodeData = { label: string; }; function MindMapNode({ id, data }: NodeProps<NodeData>) { return ( <> <input defaultValue={data.label} /> <Handle type="target" position={Position.Top} /> <Handle type="source" position={Position.Bottom} /> </> ); } export default MindMapNode;

我们正在使用一个输入框来显示和编辑思维导图节点的标签,并使用两个句柄来连接它们。这是 React Flow 工作所必需的;句柄用作边的起点和终点位置。

🌐 We are using an input for displaying and editing the labels of our mind map nodes, and two handles for connecting them. This is necessary for React Flow to work; the handles are used as the start and end position of the edges.

我们还向 index.css 文件添加了一些 CSS,以使节点看起来更漂亮一些:

🌐 We also add some CSS to the index.css file to make the nodes look a bit prettier:

src/index.css

.react-flow__node-mindmap { background: white; border-radius: 2px; border: 1px solid transparent; padding: 2px 5px; font-weight: 700; }

(关于此的更多信息,你可以阅读我们文档中的自定义节点指南。)

让我们对自定义边缘做同样的操作。在 src/App 下创建一个名为 MindMapEdge 的新文件夹,并在其中创建一个索引文件:

🌐 Let’s do the same for the custom edge. Create a new folder called MindMapEdge with an index file under src/App:

src/App/MindMapEdge/index.tsx

import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react'; function MindMapEdge(props: EdgeProps) { const { sourceX, sourceY, targetX, targetY } = props; const [edgePath] = getStraightPath({ sourceX, sourceY, targetX, targetY, }); return <BaseEdge path={edgePath} {...props} />; } export default MindMapEdge;

我将在下一节中更详细地介绍自定义节点和边。目前,重要的是我们可以在我们的应用中使用这些新类型,通过向我们的 Flow 组件添加以下内容:

🌐 I will get into more detail about the custom nodes and edges in the next section. For now it’s important that we can use the new types in our app, by adding the following to our Flow component:

import MindMapNode from './MindMapNode'; import MindMapEdge from './MindMapEdge'; const nodeTypes = { mindmap: MindMapNode, }; const edgeTypes = { mindmap: MindMapEdge, };

然后将新创建的类型传递给 React Flow 组件。

🌐 and then pass the newly created types to the React Flow component.

太好了!我们已经可以通过点击输入框并输入内容来更改节点的标签了。

🌐 Nice! We can already change the labels of our nodes by clicking in the input field and typing something.

新节点

我们希望让用户创建新节点的过程非常快速。用户应该能够通过点击一个节点并拖动到新节点应放置的位置来添加新节点。这个功能在 React Flow 中并未内置,但我们可以通过使用 onConnectStartonConnectEnd 处理程序来实现它。

🌐 We want to make it super quick for a user to create a new node. The user should be able to add a new node by clicking on a node and drag to the position where a new node should be placed. This functionality is not built into React Flow, but we can implement it by using the onConnectStart and onConnectEnd handlers.

我们使用开始处理程序来记住被单击的节点,并使用结束处理程序来创建新节点:

🌐 We are using the start handler to remember the node that was clicked and the end handler to create the new node:

添加到 src/App/index.tsx

🌐 Add to src/App/index.tsx

const connectingNodeId = useRef<string | null>(null); const onConnectStart: OnConnectStart = useCallback((_, { nodeId }) => { connectingNodeId.current = nodeId; }, []); const onConnectEnd: OnConnectEnd = useCallback((event) => { // we only want to create a new node if the connection ends on the pane const targetIsPane = (event.target as Element).classList.contains('react-flow__pane'); if (targetIsPane && connectingNodeId.current) { console.log(`add new node with parent node ${connectingNodeId.current}`); } }, []);

由于我们的节点由存储管理,我们创建了一个动作来添加新节点及其边。这就是我们的 addChildNode 动作的样子:

🌐 Since our nodes are managed by the store, we create an action to add a new node and its edge. This is how our addChildNode action looks:

src/store.ts 中的新操作

🌐 New action in src/store.ts

addChildNode: (parentNode: Node, position: XYPosition) => { const newNode = { id: nanoid(), type: 'mindmap', data: { label: 'New Node' }, position, parentNode: parentNode.id, }; const newEdge = { id: nanoid(), source: parentNode.id, target: newNode.id, }; set({ nodes: [...get().nodes, newNode], edges: [...get().edges, newEdge], }); };

我们正在将传入的节点用作父节点。通常此功能用于实现分组子流程。在这里,我们使用它来在父节点移动时移动所有子节点。它使我们能够整理和重新排列思维导图,这样就不必手动移动所有子节点。让我们在 onConnectEnd 处理程序中使用这个新操作:

🌐 We are using the passed node as a parent. Normally this feature is used to implement grouping or sub flows. Here we are using it to move all child nodes when their parent is moved. It enables us to clean up and re-order the mind map so that we don’t have to move all child nodes manually. Let’s use the new action in our onConnectEnd handler:

调整 src/App/index.tsx

🌐 Adjustments in src/App/index.tsx

const store = useStoreApi(); const onConnectEnd: OnConnectEnd = useCallback( (event) => { const { nodeLookup } = store.getState(); const targetIsPane = (event.target as Element).classList.contains('react-flow__pane'); if (targetIsPane && connectingNodeId.current) { const parentNode = nodeLookup.get(connectingNodeId.current); const childNodePosition = getChildNodePosition(event, parentNode); if (parentNode && childNodePosition) { addChildNode(parentNode, childNodePosition); } } }, [getChildNodePosition], );

首先,我们通过 store.getState() 从 React Flow 存储中获取 nodeLookupnodeLookup 是一个包含所有节点及其当前状态的映射。我们需要它来获取被点击节点的位置和尺寸。然后我们检查 onConnectEnd 事件的目标是否是 React Flow 面板。如果是,我们希望添加一个新节点。为此,我们使用了 addChildNode 和新创建的 getChildNodePosition 辅助函数。

🌐 First we are getting the nodeLookup from the React Flow store via store.getState(). nodeLookup is a map that contains all nodes and their current state. We need it to get the position and dimensions of the clicked node. Then we check if the target of the onConnectEnd event is the React Flow pane. If it is, we want to add a new node. For this we are using our addChildNode and the newly created getChildNodePosition helper function.

src/App/index.tsx 中的辅助函数

🌐 Helper function in src/App/index.tsx

const getChildNodePosition = (event: MouseEvent, parentNode?: Node) => { const { domNode } = store.getState(); if ( !domNode || // we need to check if these properties exist, because when a node is not initialized yet, // it doesn't have a positionAbsolute nor a width or height !parentNode?.computed?.positionAbsolute || !parentNode?.computed?.width || !parentNode?.computed?.height ) { return; } const panePosition = screenToFlowPosition({ x: event.clientX, y: event.clientY, }); // we are calculating with positionAbsolute here because child nodes are positioned relative to their parent return { x: panePosition.x - parentNode.computed?.positionAbsolute.x + parentNode.computed?.width / 2, y: panePosition.y - parentNode.computed?.positionAbsolute.y + parentNode.computed?.height / 2, }; };

此函数返回我们想要添加到存储中的新节点的位置。我们使用 project 函数 将屏幕坐标转换为 React Flow 坐标。如前所述,子节点的位置是相对于其父节点的。这就是为什么我们需要从子节点的位置中减去父节点的位置。这内容有点多,让我们通过实际操作来看一下:

🌐 This function returns the position of the new node we want to add to our store. We are using the project function to convert screen coordinates into React Flow coordinates. As mentioned earlier, child nodes are positioned relative to their parents. That’s why we need to subtract the parent position from the child node position. That was a lot to take in, let’s see it in action:

要测试新功能,你可以从一个句柄开始连接,然后在面板上结束连接。你应该会看到思维导图中添加了一个新的节点。

🌐 To test the new functionality you can start a connection from a handle and then end it on the pane. You should see a new node being added to the mind map.

保持数据同步

我们已经可以更新标签,但我们还没有更新节点数据对象。这对于保持应用同步非常重要,例如如果我们想将节点保存到服务器上。为了实现这一点,我们在存储中添加了一个名为 updateNodeLabel 的新操作。该操作接受一个节点 ID 和一个标签。实现非常简单:我们遍历现有节点,并用传入的标签更新匹配的节点:

🌐 We can already update the labels but we are not updating the nodes data object. This is important to keep our app in sync and if we want to save our nodes on the server for example. To achieve this we add a new action called updateNodeLabel to the store. This action takes a node id and a label. The implementation is pretty straight forward: we iterate over the existing nodes and update the matching one with the passed label:

src/store.ts

updateNodeLabel: (nodeId: string, label: string) => { set({ nodes: get().nodes.map((node) => { if (node.id === nodeId) { // it's important to create a new object here, to inform React Flow about the changes node.data = { ...node.data, label }; } return node; }), }); },

让我们在我们的 MindmapNode 组件中使用新的操作:

🌐 Let’s use the new action in our MindmapNode component:

src/App/MindmapNode/index.tsx

import { Handle, NodeProps, Position } from '@xyflow/react'; import useStore from '../store'; export type NodeData = { label: string; }; function MindMapNode({ id, data }: NodeProps<NodeData>) { const updateNodeLabel = useStore((state) => state.updateNodeLabel); return ( <> <input // from now on we can use value instead of defaultValue // this makes sure that the input always shows the current label of the node value={data.label} onChange={(evt) => updateNodeLabel(id, evt.target.value)} className="input" /> <Handle type="target" position={Position.Top} /> <Handle type="source" position={Position.Top} /> </> ); } export default MindMapNode;

真快!自定义节点的输入字段现在显示节点的当前标签。你可以获取节点数据,将其保存在服务器上,然后再次加载。

🌐 That was quick! The input fields of the custom nodes now display the current label of the nodes. You could take your nodes data, save it on the server and then load it again.

更简洁的用户体验和更美观的样式

在功能方面,我们的思维导图应用已经完成了!我们可以添加新节点,更新它们的标签,并移动它们。但用户体验和样式还可以改进。让我们让拖动节点和创建新节点更容易!

🌐 Functionality-wise we are finished with our mind map app! We can add new nodes, update their labels and move them around. But the UX and styling could use some improvements. Let’s make it easier to drag the nodes and to create new nodes!

1. 作为句柄的节点

🌐 1. A node as handle

让我们将整个节点用作句柄,而不是显示默认句柄。这使得创建节点更容易,因为可以开始新连接的区域变大了。我们需要将源句柄的大小设置为节点的大小,并在视觉上隐藏目标句柄。React Flow 仍然需要它来连接节点,但我们不需要显示它,因为我们是通过在面板上拖放边来创建新节点的。我们使用普通的 CSS 来隐藏目标句柄并将其定位在节点的中心:

🌐 Let’s use the whole node as a handle, rather than displaying the default handles. This makes it easier to create nodes, because the area where you can start a new connection gets bigger. We need to style the source handle to be the size of the node and hide the target handle visually. React Flow still needs it to connect the nodes but we don’t need to display it since we are creating new nodes by dropping an edge on the pane. We use plain old CSS to hide the target handle and position it in the center of the node:

src/index.css

.react-flow__handle.target { top: 50%; pointer-events: none; opacity: 0; }

为了使整个节点成为句柄,我们还更新了源的样式:

🌐 In order to make the whole node a handle, we also update the style of the source:

src/index.css

.react-flow__handle.source { top: 0; left: 0; transform: none; background: #f6ad55; height: 100%; width: 100%; border-radius: 2px; border: none; }

这可以工作,但我们不能再移动节点了,因为源句柄现在是整个节点,并覆盖了输入字段。我们通过使用 dragHandle 节点选项 来解决这个问题。它允许我们为应该用作拖动句柄的 DOM 元素指定选择器。为此,我们稍微调整自定义节点:

🌐 This works but we can’t move the nodes anymore because the source handle is now the whole node and covers the input field. We fix that by using the dragHandle node option. It allows us to specify a selector for a DOM element that should be used as a drag handle. For this we adjust the custom node a bit:

src/App/MindmapNode/index.tsx

import { Handle, NodeProps, Position } from '@xyflow/react'; import useStore from '../store'; export type NodeData = { label: string; }; function MindMapNode({ id, data }: NodeProps<NodeData>) { const updateNodeLabel = useStore((state) => state.updateNodeLabel); return ( <> <div className="inputWrapper"> <div className="dragHandle"> <svg viewBox="0 0 24 24"> <path fill="#333" stroke="#333" strokeWidth="1" d="M15 5h2V3h-2v2zM7 5h2V3H7v2zm8 8h2v-2h-2v2zm-8 0h2v-2H7v2zm8 8h2v-2h-2v2zm-8 0h2v-2H7v2z" /> </svg> </div> <input value={data.label} onChange={(evt) => updateNodeLabel(id, evt.target.value)} className="input" /> </div> <Handle type="target" position={Position.Top} /> <Handle type="source" position={Position.Top} /> </> ); } export default MindMapNode;

我们添加一个类名为 inputWrapper 的封装 div,以及一个作为拖动句柄的类名为 dragHandle 的 div(惊喜!)。现在我们可以为新元素添加样式了:

🌐 We add a wrapper div with the class name inputWrapper and a div with the class name dragHandle that acts as the drag handle (surprise!). Now we can style the new elements:

src/index.css

.inputWrapper { display: flex; height: 20px; z-index: 1; position: relative; } .dragHandle { background: transparent; width: 14px; height: 100%; margin-right: 4px; display: flex; align-items: center; } .input { border: none; padding: 0 2px; border-radius: 1px; font-weight: 700; background: transparent; height: 100%; color: #222; }

2. 焦点时激活输入

🌐 2. Activate input on focus

我们几乎完成了,但我们还需要调整一些细节。我们希望从节点的中心开始我们的新连接。为此,我们将输入的指针事件设置为“none”,并检查用户是否在节点上方释放按钮。只有在这种情况下,我们才希望激活输入字段。我们可以使用我们的 onConnectEnd 函数来实现这一点:

🌐 We are almost there but we need to adjust some more details. We want to start our new connection from the center of the node. For this we set the pointer events of the input to “none” and check if the user releases the button on top of the node. Only then we want to activate the input field. We can use our onConnectEnd function to achieve this:

src/App/index.tsx

const onConnectEnd: OnConnectEnd = useCallback( (event) => { const { nodeLookup } = store.getState(); const targetIsPane = (event.target as Element).classList.contains('react-flow__pane'); const node = (event.target as Element).closest('.react-flow__node'); if (node) { node.querySelector('input')?.focus({ preventScroll: true }); } else if (targetIsPane && connectingNodeId.current) { const parentNode = nodeLookup.get(connectingNodeId.current); const childNodePosition = getChildNodePosition(event, parentNode); if (parentNode && childNodePosition) { addChildNode(parentNode, childNodePosition); } } }, [getChildNodePosition], );

如你所见,当用户在节点上释放鼠标按钮时,我们正在聚焦输入字段。我们现在可以添加一些样式,以便输入字段仅在聚焦时(pointerEvents: all)被激活:

🌐 As you see we are focusing the input field if the user releases the mouse button on top of a node. We can now add some styling so that the input field is activated (pointerEvents: all) only when it’s focused:

/* we want the connection line to be below the node */ .react-flow .react-flow__connectionline { z-index: 0; } /* pointer-events: none so that the click for the connection goes through */ .inputWrapper { display: flex; height: 20px; position: relative; z-index: 1; pointer-events: none; } /* pointer-events: all so that we can use the drag handle (here the user cant start a new connection) */ .dragHandle { background: transparent; width: 14px; height: 100%; margin-right: 4px; display: flex; align-items: center; pointer-events: all; } /* pointer-events: none by default */ .input { border: none; padding: 0 2px; border-radius: 1px; font-weight: 700; background: transparent; height: 100%; color: #222; pointer-events: none; } /* pointer-events: all when it's focused so that we can type in it */ .input:focus { border: none; outline: none; background: rgba(255, 255, 255, 0.25); pointer-events: all; }

3. 动态宽度和自动聚焦

🌐 3. Dynamic width and auto focus

几乎完成了!我们希望根据文本的长度为节点设置动态宽度。为了简化,我们根据文本的长度进行如下计算:

🌐 Almost done! We want to have a dynamic width for the nodes based on the length of the text. To keep it simple we do a calculation based on the length of text for this:

在 src/app/MindMapNode.tsx 中添加效果

🌐 Added effect in src/app/MindMapNode.tsx

useLayoutEffect(() => { if (inputRef.current) { inputRef.current.style.width = `${data.label.length * 8}px`; } }, [data.label.length]);

我们还想在创建节点后立即聚焦/激活它:

🌐 We also want to focus / activate a node right after it gets created:

在 src/app/MindMapNode.tsx 中添加效果

🌐 Added effect in src/app/MindMapNode.tsx

useEffect(() => { setTimeout(() => { if (inputRef.current) { inputRef.current.focus({ preventScroll: true }); } }, 1); }, []);

现在,当你调整节点标签时,节点的宽度会相应调整。你还可以创建一个新节点,它会立即被聚焦。

🌐 Now when you adjust a node label, the width of the node will adjust accordingly. You can also create a new node and it will be focused right away.

4. 居中的边缘和样式细节

🌐 4. Centered edges and styling details

你可能已经注意到边缘没有居中。我们在一开始为此创建了一个自定义边缘,现在我们可以稍微调整它,使边缘从节点的中心开始,而不是从句柄的顶部开始(默认行为):

🌐 You may have noticed that the edges are not centered. We created a custom edge at the beginning for this, and now we can adjust it a bit so that the edge starts in the center of the node and not at the top of the handle (the default behavior):

src/App/MindMapEdge.tsx

import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react'; function MindMapEdge(props: EdgeProps) { const { sourceX, sourceY, targetX, targetY } = props; const [edgePath] = getStraightPath({ sourceX, sourceY: sourceY + 20, targetX, targetY, }); return <BaseEdge path={edgePath} {...props} />; } export default MindMapEdge;

我们将所有属性传递给 getStraightPath 辅助函数,但调整 sourceY,使其位于节点的中心。

🌐 We are passing all props to the getStraightPath helper function but adjust the sourceY so that it is in the center of the node.

此外,我们希望标题更为低调,并为背景选择一种颜色。我们可以通过调整面板的颜色(我们添加了类名 "header")以及 body 元素的背景颜色来实现这一点:

🌐 More over we want the title to be a bit more subtle and choose a color for our background. We can do this by adjusting the color of the panel (we added the class name "header") and the background color of the body element:

body { margin: 0; background-color: #f8f8f8; height: 100%; } .header { color: #cdcdcd; }

干得好! 你可以在这里找到最终代码:

最后的想法

太棒了!我们从一个空白面板开始,最后完成了一个功能齐全的思维导图应用。如果你想继续,你可以开发以下一些功能:

🌐 What a trip! We started with an empty pane and ended with a fully functional mind map app. If you want to move on you could work on some of the following features:

  • 通过单击窗格添加新节点
  • 保存和恢复按钮将当前状态存储到本地存储
  • 导出和导入 UI
  • 协作编辑

我希望你喜欢本教程并学到了新知识!如果你有任何问题或反馈,请随时通过Twitter 与我联系,或加入我们的Discord服务器 。React Flow 是一家由用户资助的独立公司。如果你想支持我们,可以在GitHub上赞助我们 订阅我们的高级计划

🌐 I hope you enjoyed this tutorial and learned something new! If you have any questions or feedback, feel free to reach out to me on Twitter  or join our Discord server . React Flow is an independent company financed by its users. If you want to support us you can sponsor us on Github  or subscribe to one of our Pro plans.

Get Pro examples, prioritized bug reports, 1:1 support from the maintainers, and more with React Flow Pro

Last updated on