使用React Hooks + Typescript 开发一颗简易的树
一、搭建Reac18.* + Vite工程
二、编码思路
- 需要一个树容器
Tree组件,去管理数据data(初始化、新增、替换、删除); - 需要一个树节点
TreeNode组件,根据data渲染整颗树结构,定义树节点样式、子节点样式、图标样式; - 需要提前定义好树节点的接口
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 Props3、不出意外的话,马上就要出意外了----你得树现在应该长这样:
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、页面渲染
此时你的页面应该长这样
且展开收起正常执行
六、补充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;