Contents

React中编写操作树形数据的自定义Hook

什么是 Hook

hook 即为钩子,是一种特殊的函数,它可以让你在函数式组件中使用一些 react 特性,目前在 react 中常用的 hook 有以下几类

  • useState: 用于在函数组件中定义和使用状态(state)。
  • useEffect:用于在函数组件中处理副作用,也可以模拟 react 生命周期
  • useContext:用于在函数组件中访问 React 的上下文(context)。
  • useCallback:用于在函数组件中缓存计算结果,避免无用的重复计算。
  • useMemo:用于在函数组件中缓存回调函数,避免无用的重渲染。

以上各种 hook 的用法在笔记文档中均有记录,如有兴趣可以前往阅览.

自定义 Hook

自定义 Hook 是指在 React 中编写的自定义函数,以便在各个组件之间重用逻辑。通过自定义 Hook,我们可以将一些逻辑抽象出来,使它们可以在不同的组件中共享和复用。

自定义 Hook 的命名以 “use” 开头,这是为了遵循 React 的 Hook 命名规范。自定义 Hook 可以使用任何 React 的内置 Hook,也可以组合其他自定义 Hook。

编写自定义 Hook

那么如何编写自定义 hook 呢,且看以下场景:

在 Antd 中有一个 Tree 组件,现在需要对 Tree 组件的数据进行操作来方便我们在 Tree 中插入,更新,上移,下移,删除节点,此时我们就可以编写一个自定义 hook 来统一操作类似于 TreeData 这样的树形数据

我们在此将这个 hook 函数其命名为 useTreeHandler,编写这个自定义 hook 函数只需要三步同时

  • 保存传入的数据
  • 为传入的数据编写操作函数
  • 将操作后的数据以及函数暴露出去供组件使用
const useTreeHandler = (TreeData: DataNode[]) => {
  const [gData, setGData] = useState(JSON.parse(JSON.stringify(TreeData)));
  return {
    gData,
  };
};

因为本次操作的是类似 Antd 中的树形数据,就暂且使用 DataNode 类型,当然这个类型可以根据我们的需要来设定或者写一个更加通用的类型 在此 hook 函数中我们要实现以下功能

  • insertNodeByKey: 根据 key 来插入子级节点
  • insertNodeInParentByKey: 根据 key 来插入同级节点
  • deleteNodeByKey: 根据 key 来删除当前节点
  • updateTreeDataByKey: 根据 key 来更新当前节点
  • moveNodeInTreeByKey: 根据 key 上移/下移当前节点

插入子级

/**
 * 插入子级
 * @param key 当前节点key
 * @param newNode 待插入节点
 */
const insertNodeByKey = function (
  key: string | number | undefined,
  newNode: any
) {
  const data = JSON.parse(JSON.stringify(gData));
  const insertChild = (
    data: any[],
    key: string | number | undefined,
    newNode: any
  ): any[] => {
    for (let i = 0; i < data.length; i++) {
      if (data[i].key === key) {
        if (Array.isArray(data[i].children)) {
          data[i].children = [...data[i].children, newNode];
        } else {
          data[i].children = [newNode];
        }
        break;
      } else if (Array.isArray(data[i].children)) {
        insertChild(data[i].children, key, newNode);
      }
    }
    return data;
  };
  setGData(insertChild(data, key, newNode));
};

上述insertNodeByKey函数代码中传入了两个参数keynewNode,这两个分别代表当前操作节点对象的 key 以及插入的新节点数据,在insertNodeByKey函数内部对 gData 进行了一次深拷贝,之后在函数内操作深拷贝之后的数据,接着又定义了一个inserChild函数此函数主要进行数据操作,最后将操作后的数据重新赋值给 gData,在inserChild函数中首先对数组数据进行循环遍历,检查每一项的 key 是否和目标 key 相同,如果相同的话将新节点数据插入到当前遍历的节点的children中并break跳出循环,没有找到的话进行递归. 接下来更新节点,删除节点,上移/下移的函数和插入节点函数思路相同,在此就不一一解释,如下直接贴上代码:

插入同级

/**
 * 插入同级
 * @param key 当前节点key 供查询父key
 * @param newNode 新节点数据
 */
const insertNodeInParentByKey = function (
  key: string | number | undefined,
  newNode: any
) {
  const data = JSON.parse(JSON.stringify(gData));
  const insertBro = (
    data: any[],
    key: string | number | undefined,
    newNode: any
  ) => {
    for (let i = 0; i < data.length; i++) {
      const item = data[i];
      if (item.children) {
        for (let j = 0; j < item.children.length; j++) {
          const childItem = item.children[j];
          if (childItem.key === key) {
            item.children.push(newNode);
            break;
          } else if (childItem.children) {
            insertBro([childItem], key, newNode);
          }
        }
      }
    }
    return data;
  };
  setGData(insertBro(data, key, newNode));
};

删除当前节点

/**
 * 删除当前节点
 * @param data 源数据
 * @param key 待删除节点key
 */
const deleteNodeByKey = function (key: string | number | undefined) {
  const data = JSON.parse(JSON.stringify(gData));
  const delNode = (data: any[], key: string | number | undefined) => {
    for (let i = 0; i < data.length; i++) {
      const obj = data[i];
      if (obj.key === key) {
        data.splice(i, 1);
        break;
      } else if (obj.children) {
        delNode(obj.children, key);
        if (obj.children.length === 0) {
          delete obj.children;
        }
      }
    }
  };
  delNode(data, key);
  setGData(data);
};

更新当前节点

/**
 * 更新子节点配置
 * @param oldData 旧数据
 * @param key 待更新子节点key
 * @param newData 更新后新数据
 */
const updateTreeDataByKey = function (
  key: string | number | undefined,
  newData: any
) {
  const data = JSON.parse(JSON.stringify(gData));
  const updateNode = (
    oldData: any[],
    key: string | number | undefined,
    newData: any[]
  ) => {
    for (let i = 0; i < oldData.length; i++) {
      if (oldData[i].key === key) {
        oldData[i] = { ...oldData[i], ...newData };
        break;
      } else {
        if (Array.isArray(oldData[i].children)) {
          updateNode(oldData[i].children, key, newData);
        }
      }
    }
  };
  updateNode(data, key, newData);
  setGData(data);
};

当前节点上移/下移

/**
 * 上移/下移
 * @param data 源数据
 * @param key 目标key
 * @param direction 移动类型
 * @returns 更新后数据
 */
const moveNodeInTreeByKey = function (
  key: string | number | undefined,
  direction: "UP" | "DOWN"
) {
  const data = JSON.parse(JSON.stringify(gData));
  const moveNode = (
    data: any[],
    key: string | number | undefined,
    direction: string
  ) => {
    const newData = [...data];
    for (let i = 0; i < newData.length; i++) {
      const item = newData[i];
      const itemLen = item.children.length;
      if (item.children) {
        for (let j = 0; j < itemLen; j++) {
          const childItem = item.children[j];
          if (childItem.key === key) {
            if (j === 0 && direction === "UP")
              // message.info("已经处于第一位,无法上移");
              message.info({
                content: "已经处于第一位,无法上移",
                className: "custom-class",
                style: {
                  marginTop: "5vh",
                  position: "absolute",
                  right: 20,
                  textAlign: "center",
                },
              });
            if (j === itemLen - 1 && direction === "DOWN")
              // message.info("已经处于最后一位,无法下移");
              message.info({
                content: "已经处于最后一位,无法下移",
                className: "custom-class",
                style: {
                  marginTop: "5vh",
                  position: "absolute",
                  right: 20,
                  textAlign: "center",
                },
              });
            // splice (开始位置,移除元素个数,新增元素对象)
            if (direction === "UP") {
              item.children.splice(j, 1);
              item.children.splice(j - 1, 0, childItem);
            } else {
              item.children.splice(j, 1);
              item.children.splice(j + 1, 0, childItem);
            }

            break;
          } else if (childItem.children) {
            moveNode([childItem], key, direction);
          }
        }
      }
    }
    return newData;
  };
  setGData(moveNode(data, key, direction));
};

完整的 hook 函数

const useTreeHandler = (TreeData: DataNode[]) => {
  const [gData, setGData] = useState(JSON.parse(JSON.stringify(TreeData)));
  /**
   * 插入子级
   * @param key 当前节点key
   * @param newNode 待插入节点
   */
  const insertNodeByKey = function (
    key: string | number | undefined,
    newNode: any
  ) {
    const data = JSON.parse(JSON.stringify(gData));
    const insertChild = (
      data: any[],
      key: string | number | undefined,
      newNode: any
    ): any[] => {
      for (let i = 0; i < data.length; i++) {
        if (data[i].key === key) {
          if (Array.isArray(data[i].children)) {
            data[i].children = [...data[i].children, newNode];
          } else {
            data[i].children = [newNode];
          }
          break;
        } else if (Array.isArray(data[i].children)) {
          insertChild(data[i].children, key, newNode);
        }
      }
      return data;
    };
    setGData(insertChild(data, key, newNode));
  };

  /**
   * 插入同级
   * @param key 当前节点key 供查询父key
   * @param newNode 新节点数据
   */
  const insertNodeInParentByKey = function (
    key: string | number | undefined,
    newNode: any
  ) {
    const data = JSON.parse(JSON.stringify(gData));
    const insertBro = (
      data: any[],
      key: string | number | undefined,
      newNode: any
    ) => {
      for (let i = 0; i < data.length; i++) {
        const item = data[i];
        if (item.children) {
          for (let j = 0; j < item.children.length; j++) {
            const childItem = item.children[j];
            if (childItem.key === key) {
              item.children.push(newNode);
              break;
            } else if (childItem.children) {
              insertBro([childItem], key, newNode);
            }
          }
        }
      }
      return data;
    };
    setGData(insertBro(data, key, newNode));
  };
  /**
   * 删除当前节点
   * @param data 源数据
   * @param key 待删除节点key
   */
  const deleteNodeByKey = function (key: string | number | undefined) {
    const data = JSON.parse(JSON.stringify(gData));
    const delNode = (data: any[], key: string | number | undefined) => {
      for (let i = 0; i < data.length; i++) {
        const obj = data[i];
        if (obj.key === key) {
          data.splice(i, 1);
          break;
        } else if (obj.children) {
          delNode(obj.children, key);
          if (obj.children.length === 0) {
            delete obj.children;
          }
        }
      }
    };
    delNode(data, key);
    setGData(data);
  };
  /**
   * 更新子节点配置
   * @param oldData 旧数据
   * @param key 待更新子节点key
   * @param newData 更新后新数据
   */
  const updateTreeDataByKey = function (
    key: string | number | undefined,
    newData: any
  ) {
    const data = JSON.parse(JSON.stringify(gData));
    const updateNode = (
      oldData: any[],
      key: string | number | undefined,
      newData: any[]
    ) => {
      for (let i = 0; i < oldData.length; i++) {
        if (oldData[i].key === key) {
          oldData[i] = { ...oldData[i], ...newData };
          break;
        } else {
          if (Array.isArray(oldData[i].children)) {
            updateNode(oldData[i].children, key, newData);
          }
        }
      }
    };
    updateNode(data, key, newData);
    setGData(data);
  };
  /**
   * 上移/下移
   * @param data 源数据
   * @param key 目标key
   * @param direction 移动类型
   */
  const moveNodeInTreeByKey = function (
    key: string | number | undefined,
    direction: "UP" | "DOWN"
  ) {
    const data = JSON.parse(JSON.stringify(gData));
    const moveNode = (
      data: any[],
      key: string | number | undefined,
      direction: string
    ) => {
      const newData = [...data];
      for (let i = 0; i < newData.length; i++) {
        const item = newData[i];
        const itemLen = item.children.length;
        if (item.children) {
          for (let j = 0; j < itemLen; j++) {
            const childItem = item.children[j];
            if (childItem.key === key) {
              if (j === 0 && direction === "UP")
                message.info("已经处于第一位,无法上移");
              if (j === itemLen - 1 && direction === "DOWN")
                message.info("已经处于最后一位,无法下移");
              // splice (开始位置,移除元素个数,新增元素对象)
              if (direction === "UP") {
                item.children.splice(j, 1);
                item.children.splice(j - 1, 0, childItem);
              } else {
                item.children.splice(j, 1);
                item.children.splice(j + 1, 0, childItem);
              }

              break;
            } else if (childItem.children) {
              moveNode([childItem], key, direction);
            }
          }
        }
      }
      return newData;
    };
    setGData(moveNode(data, key, direction));
  };
  return {
    gData,
    insertNodeByKey,
    insertNodeInParentByKey,
    deleteNodeByKey,
    updateTreeDataByKey,
    moveNodeInTreeByKey,
  };
};

写在最后

演示地址

完整代码