React中編寫操作樹形資料的自定義Hook

2023-07-12 12:02:46

什麼是 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,
  };
};

寫在最後

演示地址

完整程式碼