使用React Hooks + Typescript 开发一颗简易的树

144 阅读4分钟

使用React Hooks + Typescript 开发一颗简易的树

一、搭建Reac18.* + Vite工程

搭建React18.*+Vite工程

二、编码思路

  1. 需要一个树容器Tree组件,去管理数据data(初始化、新增、替换、删除);
  2. 需要一个树节点TreeNode组件,根据data渲染整颗树结构,定义树节点样式、子节点样式、图标样式;
  3. 需要提前定义好树节点的接口interface TreeData,保障树的稳定结构;

三、定义树节点项的接口 TreeData

// src\typings\index.d.ts

interface TreeData {
  key: string; // 唯一key值
  name: string; // 名称
  checked: boolean; // 是否选中
  expand: boolean; // 是否展开
  children?: TreeData[]; // 子树
  parent?: TreeData | null; // 父树
}

四、实现

4.0 从App.tsx引入文件与传入数据,treeData 后续补充,否则篇章看起来很燥

import './App.css';
import Tree from './components/tree';

const treeData: TreeData[] = []; // 后续补充

function App() {
  return (
    <>
      <div
        style={{
          width: '300px',
          border: '1px solid #222',
          height: '500px',
          overflow: 'auto',
        }}
      >
        <Tree data={treeData}></Tree>
      </div>
    </>
  );
}

export default App;

4.1 初始化 tree.tsx

因为Tree组件负责管理数据,所以TreeNode触发的事件需要冒泡到这一层来处理,更新数据,渲染视图。

import { FC, useEffect, useState } from 'react';

// 定义props
interface Props {
  data: TreeData[];
}

const Tree: FC<Props> = ({ data }) => {
  const [_data, setData] = useState<TreeData[]>(data);
  
  // key节点触发checked
  const onCheck = (key: string) => {
    return true;
  };
  
  // key节点触发expand
  const onExpand = (key: string) => {
    return true;
  };
  
  return (
    <>
      <TreeNode data={_data} onCheck={onCheck} onExpand={onExpand}></TreeNode>
    </>
  );
};

export default Tree;

4.2 初始化 tree.node.tsx

这里已经可以看到树结构雏形已初具成效。

代码简单清晰,通过遍历node.children来生成新的TreeNode组件。

import { FC } from 'react';
import styles from './tree.node.module.less';

interface Props {
  data: TreeData[];
  onCheck?: (key: string) => boolean;
  onExpand?: (key: string) => boolean;
}

const TreeNode: FC<Props> = ({ data, onCheck, onExpand }) => {
  return (
    <>
      {data.map((node) => {
        return (
          <div key={node.key} className={styles.node}>
            <div className={styles.name}>{node.name}</div>
            {node.children ? (
              <div className="node-children">
                <TreeNode
                  // 这里很重要把children传入进去后不用再关心children的死活它将作为子TreeNode的data-Props属性传入
                  data={node.children}
                  onCheck={onCheck}
                  onExpand={onExpand}
                ></TreeNode>
              </div>
            ) : null}
          </div>
        );
      })}
    </>
  );
};

export default TreeNode;

4.3 页面渲染

1、记得将styles.node增加样式padding: 0 12px;这样你的树看起来会有父子分层的感觉

2、补充App.tsx中的data Props

3、不出意外的话,马上就要出意外了----你得树现在应该长这样:

image.png

4.4 data Mock

// data Demo
const treeData: TreeData[] = [
  {
    key: '1',
    name: 'Tree Node 1',
    checked: false,
    expand: true,
    children: [
      {
        key: '1-1',
        name: 'Tree Node 1-1',
        checked: false,
        expand: true,
        children: [
          {
            key: '1-1-1',
            name: 'Tree Node 1-1-1',
            checked: false,
            expand: true,
          },
        ],
      },
      {
        key: '1-2',
        checked: false,
        expand: true,
        name: 'Tree Node 1-2',
        children: [
          {
            key: '2-1',
            name: 'Tree Node 2-1',
            checked: false,
            expand: true,
          },
          {
            key: '2-2',
            name: 'Tree Node 2-2',
            checked: false,
            expand: true,
            children: [
              {
                key: '2-2-1',
                name: 'Tree Node 2-1',
                checked: false,
                expand: true,
              },
            ],
          },
        ],
      },
    ],
  },
];

五、补充Expand

5.1 treeNode

// src\components\tree.node.tsx

import { FC } from 'react';
import styles from './tree.node.module.less';

interface Props {
  data: TreeData[];
  onCheck?: (key: string) => boolean;
  onExpand?: (key: string) => boolean;
}

const TreeNode: FC<Props> = ({ data, onCheck, onExpand }) => {
  return (
    <>
      {data.map((node) => {
        return (
          <div key={node.key} className={styles.node}>
               /**新增代码块 */
                <div className={styles.item}>
              {node.children ? (
                <div
                  className={styles.icon}
                  onClick={() => onExpand && onExpand(node.key)}
                >
                  {node.expand ? (
                    <IconMdiArrowDownDropCircleOutline />
                  ) : (
                    <IconMdiArrowRightDropCircleOutline />
                  )}
                </div>
              ) : null}
               /**新增代码块结束 */
              <div className={styles.name}>{node.name}</div>
            </div>
            {node.expand && node.children ? (
              <div className="node-children">
                <TreeNode
                  data={node.children}
                  onCheck={onCheck}
                  onExpand={onExpand}
                ></TreeNode>
              </div>
            ) : null}
          </div>
        );
      })}
    </>
  );
};

export default TreeNode;

5.2 tree.tsx

// src\components\tree.tsx
import { FC, useEffect, useState } from 'react';
import TreeNode from './tree.node';

interface Props {
  data: TreeData[];
}

/** 新增代码块 */
type TreeKeyMap = Record<string, TreeData>;

/** 新增代码块 */
/**
 * 序列化整颗树,方便遍历
 * @param data
 * @param result
 * @returns
 */
const buildKeyMap = (
  data: TreeData[],
  result: TreeKeyMap = {}, // 返回结果map
  parent: TreeData | null = null // 生成父节点
): TreeKeyMap => {
  data.forEach((node) => {
    node.parent = parent;
    result[node.key] = node;
    
    // 迭代子孩子节点
    node.children && buildKeyMap(node.children, result, node);
  });
  return result;
};

const Tree: FC<Props> = ({ data }) => {
  const [_data, setData] = useState<TreeData[]>(data);
  /** 新增代码块 */
  const map = buildKeyMap(data);
  const [treeKeyMap, setTreeKeyMap] = useState<TreeKeyMap>(map);
  
 
  /** 新增代码块 */
  /** 新增内部数据的副作用 */
  useEffect(() => {
    setTreeKeyMap(buildKeyMap(_data));
  }, [_data]);

  const onCheck = () => {};
  
  /** 新增代码块 */
  const onExpand = (key: string) => {
    const node = treeKeyMap[key];
    const expand = !node.expand;
    node.expand = expand;
    setData([..._data]);
    return true;
  };

  return (
    <>
      <TreeNode data={_data} onCheck={onCheck} onExpand={onExpand}></TreeNode>
    </>
  );
};

export default Tree;

5.3、页面渲染

此时你的页面应该长这样

且展开收起正常执行

image.png

六、补充Check

TreeNode

import { FC } from 'react';
import styles from './tree.node.module.less';

interface Props {
  data: TreeData[];
  onCheck?: (key: string) => boolean;
  onExpand?: (key: string) => boolean;
}

const TreeNode: FC<Props> = ({ data, onCheck, onExpand }) => {
  return (
    <>
      {data.map((node) => {
        return (
          <div key={node.key} className={styles.node}>
            <div className={styles.item}>
              {/* 拥有子孩子的节点才可以展开 */}
              {node.children ? (
                <div
                  className={styles.icon}
                  onClick={() => onExpand && onExpand(node.key)}
                >
                  {node.expand ? (
                    // 展开
                    <IconMdiArrowDownDropCircleOutline />
                  ) : (
                    // 收起
                    <IconMdiArrowRightDropCircleOutline />
                  )}
                </div>
              ) : null}
              {/* 勾选框部分 */}
              <div className={styles.icon}>
                <input
                  checked={node.checked}
                  onChange={() => onCheck && onCheck(node.key)}
                  type="checkbox"
                ></input>
              </div>
              {/* 勾选框部分结束 */}
              <div className={styles.name}>{node.name}</div>
            </div>
            {node.expand && node.children ? (
              <div className="node-children">
                <TreeNode
                  data={node.children}
                  onCheck={onCheck}
                  onExpand={onExpand}
                ></TreeNode>
              </div>
            ) : null}
          </div>
        );
      })}
    </>
  );
};

export default TreeNode;

Tree

import { FC, useEffect, useState } from 'react';
import TreeNode from './tree.node';

interface Props {
  data: TreeData[];
}

type TreeKeyMap = Record<TreeData['key'], TreeData>;

/**
 * 序列化整颗树,方便遍历
 * @param data
 * @param result
 * @returns
 */
const buildKeyMap = (
  data: TreeData[],
  result: TreeKeyMap = {},
  parent: TreeData | null = null
): TreeKeyMap => {
  data.forEach((node) => {
    node.parent = parent;
    result[node.key] = node;
    node.children && buildKeyMap(node.children, result, node);
  });
  return result;
};

const Tree: FC<Props> = ({ data }) => {
  const map = buildKeyMap(data);
  const [treeKeyMap, setTreeKeyMap] = useState<TreeKeyMap>(map);
  const [_data, setData] = useState<TreeData[]>(data);

  // 内部_data改变时,扁平化的树结构更新
  useEffect(() => {
    setTreeKeyMap(buildKeyMap(_data));
  }, [_data]);

  // 当前被勾选时,所有子节点都应该被勾选
  // 当前被取消勾选时,所有子节点都应该被取消勾选
  const onCheckedChildrenNode = (children: TreeData[], checked: boolean) => {
    children.forEach((node) => {
      node.checked = checked;
      node.children && onCheckedChildrenNode(node.children, checked);
    });
  };

  // 当前被勾选时,
  // 深度所有父节点,并遍历其所有的子节点,判断该父节点是否充满
  const onCheckedParentAll = (parent: TreeData | null | undefined = null) => {
    // 这里通过!parent.checked进行了剪枝操作
    // 如果某一层父节点已经是checked了,那么子节点的checked对其及其再往上的节点已经没有影响了
    while (parent && !parent.checked) {
      parent.checked = (parent.children as TreeData[]).every(
        (child) => child.checked
      );
      parent = parent.parent;
    }
  };

  // 当前被取消勾选时,所有父节点都应该被取消勾选
  const onUnCheckedParent = (parent: TreeData | null | undefined = null) => {
    // 这里通过parent.checked进行了剪枝操作
    // 如果某一层父节点已经是!checked了,那么子节点的!checked对其及其再往上的节点已经没有影响了
    while (parent && parent.checked) {
      parent.checked = false;
      parent = parent.parent;
    }
  };

  const onCheck = (key: string) => {
    const node = treeKeyMap[key];
    const checked = !node.checked;

    // 这里先操作当前节点,这样后续操作其他节点时,可以不用管自身节点
    node.checked = checked;

    if (checked) {
      // 操作所有子节点
      node.children && onCheckedChildrenNode(node.children, true);
      // 操作所有父节点
      onCheckedParentAll(node.parent);
    } else {
      // 操作所有子节点
      node.children && onCheckedChildrenNode(node.children, false);
      // 操作所有父节点
      onUnCheckedParent(node.parent);
    }
    setData([..._data]);
    return true;
  };
  const onExpand = (key: string) => {
    const node = treeKeyMap[key];
    const expand = !node.expand;
    node.expand = expand;
    setData([..._data]);
    return true;
  };

  return (
    <>
      <TreeNode data={_data} onCheck={onCheck} onExpand={onExpand}></TreeNode>
    </>
  );
};

export default Tree;

七、添加展开、收起动画

八、实现异步展开

九、...