使用react-query 库实现一个树形数据的CRUD案例。
1. 安装依赖
package.json
"dependencies": {
"@tanstack/react-query": "^5.100.14",
"@tanstack/react-query-devtools": "^5.100.14",
"axios": "^1.16.1",
"react": "^19.2.6",
"react-dom": "^19.2.6"
},
2. 核心代码 useTree.js
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { fetchNodes, addNode, updateNode, deleteNode, buildTree } from '../api/treeApi'
const QUERY_KEY = ['tree-nodes']
// 查询树形数据
export const useTreeNodes = () => {
return useQuery({
queryKey: QUERY_KEY,
queryFn: fetchNodes,
select: (data) => buildTree(data),
})
}
// 新增节点
export const useAddNode = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: addNode,
onSuccess: () => queryClient.invalidateQueries({ queryKey: QUERY_KEY }),
})
}
// 更新节点
export const useUpdateNode = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updateNode,
onSuccess: () => queryClient.invalidateQueries({ queryKey: QUERY_KEY }),
})
}
// 删除节点(含子孙)
export const useDeleteNode = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: deleteNode,
onSuccess: () => queryClient.invalidateQueries({ queryKey: QUERY_KEY }),
})
}
3. 在组件中使用
import { useState } from 'react'
import { useAddNode, useUpdateNode, useDeleteNode } from '../hooks/useTree'
import './TreeNode.css'
// 生成简单唯一 id
const genId = () => Date.now().toString()
export default function TreeNode({ node, level = 0 }) {
const [expanded, setExpanded] = useState(true)
const [editing, setEditing] = useState(false)
const [editName, setEditName] = useState(node.name)
const [addingChild, setAddingChild] = useState(false)
const [newChildName, setNewChildName] = useState('')
const addNode = useAddNode()
const updateNode = useUpdateNode()
const deleteNode = useDeleteNode()
const hasChildren = node.children && node.children.length > 0
// 提交编辑
const handleUpdate = () => {
if (!editName.trim()) return
updateNode.mutate({ id: node.id, name: editName.trim() }, {
onSuccess: () => setEditing(false),
})
}
// 取消编辑
const handleCancelEdit = () => {
setEditName(node.name)
setEditing(false)
}
// 提交新增子节点
const handleAddChild = () => {
if (!newChildName.trim()) return
addNode.mutate(
{
id: genId(),
name: newChildName.trim(),
parentId: node.id,
order: (node.children?.length ?? 0) + 1,
},
{
onSuccess: () => {
setNewChildName('')
setAddingChild(false)
setExpanded(true)
},
}
)
}
// 删除节点
const handleDelete = () => {
if (!window.confirm(`确认删除「${node.name}」及其所有子节点?`)) return
deleteNode.mutate(node.id)
}
return (
<div className="tree-node" style={{ paddingLeft: level === 0 ? 0 : 20 }}>
<div className="tree-node-row">
{/* 展开/折叠按钮 */}
<span
className={`toggle ${hasChildren ? '' : 'invisible'}`}
onClick={() => setExpanded((v) => !v)}
>
{expanded ? '▼' : '▶'}
</span>
{/* 节点名称 / 编辑框 */}
{editing ? (
<span className="edit-area">
<input
value={editName}
onChange={(e) => setEditName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleUpdate()
if (e.key === 'Escape') handleCancelEdit()
}}
autoFocus
/>
<button onClick={handleUpdate} disabled={updateNode.isPending}>
{updateNode.isPending ? '保存中...' : '保存'}
</button>
<button onClick={handleCancelEdit}>取消</button>
</span>
) : (
<span className="node-name">{node.name}</span>
)}
{/* 操作按钮 */}
{!editing && (
<span className="actions">
<button onClick={() => setAddingChild((v) => !v)} title="添加子节点">
+
</button>
<button onClick={() => { setEditing(true); setEditName(node.name) }} title="编辑">
✎
</button>
<button
onClick={handleDelete}
disabled={deleteNode.isPending}
className="btn-delete"
title="删除"
>
✕
</button>
</span>
)}
</div>
{/* 新增子节点输入框 */}
{addingChild && (
<div className="add-child-row" style={{ paddingLeft: 36 }}>
<input
placeholder="输入子节点名称"
value={newChildName}
onChange={(e) => setNewChildName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleAddChild()
if (e.key === 'Escape') setAddingChild(false)
}}
autoFocus
/>
<button onClick={handleAddChild} disabled={addNode.isPending}>
{addNode.isPending ? '添加中...' : '确认'}
</button>
<button onClick={() => setAddingChild(false)}>取消</button>
</div>
)}
{/* 子节点递归渲染 */}
{expanded && hasChildren && (
<div className="children">
{node.children.map((child) => (
<TreeNode key={child.id} node={child} level={level + 1} />
))}
</div>
)}
</div>
)
}