react-query 库使用案例

使用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>
  )
}
相关推荐
小玮看世界4 分钟前
[Python] str() 和 join() 的区别与实战避坑指南
前端·javascript·python
hyf3266339 分钟前
低成本站群泛程序部署:单服务器托管百站稳定运行教程
运维·服务器·前端·搜索引擎·蜘蛛池
程序员黑豆25 分钟前
Java正则表达式详解
java·前端·ai编程
不可求~9 小时前
多模型路由怎么落地:把任务分流写进项目的最小实现
java·前端·数据库
AC赳赳老秦12 小时前
公开图片 OCR 数据提取:OpenClaw 合规采集公开信息图,识别文字与表格并转化为结构化数据
java·大数据·前端·数据库·python·php·openclaw
谢慧琼12 小时前
2026零基础做企业网站,低成本搭建官方网站
服务器·前端·javascript
weixin_4316004413 小时前
前端数据埋点(1):一次点击如何变成一条埋点
前端·js·数据埋点
IT_陈寒14 小时前
Python线程池吞了异常还不告诉我,这谁顶得住啊
前端·人工智能·后端
计算机魔术师14 小时前
同样是AI辅助建造,为什么有的游戏三个月就烂尾了?
前端