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>
  )
}
相关推荐
yuanyxh1 天前
Mac 软件推荐
前端·javascript·程序员
万少1 天前
AtomCode开发微信小程序《谁去呀》 全流程
前端·javascript·后端
某人辛木1 天前
Web自动化测试
前端·python·pycharm·pytest
Kagol1 天前
Superpowers GSD gstack AgentSkills深度测评
前端·人工智能
excel1 天前
JavaScript 字符串与模板字面量:从表象到本质理解
前端
京东云开发者1 天前
当AI成为导演-如何用AI创作动漫短剧
前端
李白的天不白1 天前
使用 SmartAdmin 进行前后端开发
java·前端
乘风gg1 天前
🤡PUA AI Coding 工具 的 10 条终极语录
前端·ai编程·claude
学Linux的语莫1 天前
Vue 3 入门教程
前端·javascript·vue.js
怕浪猫1 天前
第一章、Chrome DevTools Protocol (CDP) 详解
前端·javascript·chrome