Apifetch
import { useEffect, useState } from "react";
import "./styles.css";
const mockData = [
{
id: 1,
name: "README.md",
},
{
id: 2,
name: "Documents",
children: [
{
id: 3,
name: "Word.doc",
},
{
id: 4,
name: "Powerpoint.ppt",
},
],
},
{
id: 5,
name: "Downloads",
children: [
{
id: 6,
name: "unnamed.txt",
},
{
id: 7,
name: "Misc",
children: [
{
id: 8,
name: "foo.txt",
},
{
id: 9,
name: "bar.txt",
},
],
},
],
},
];
// --- 1. 模拟一个异步 API ---
// 这是一个更真实的 fetchData,它返回一个 Promise,并模拟了网络延迟和可能的失败。
function fetchData() {
console.log("Fetching data...");
return new Promise((resolve, reject) => {
setTimeout(() => {
// 模拟 5% 的概率请求失败
if (Math.random() > 0.05) {
// 深拷贝一份数据,避免后续操作(如排序)意外修改原始数据
const dataCopy = JSON.parse(JSON.stringify(mockData));
resolve(dataCopy);
} else {
reject(new Error("Failed to fetch data from the server."));
}
}, 1000); // 模拟 1 秒的网络延迟
});
}
// --- 2. 创建一个可递归的、负责渲染文件/文件夹的组件 ---
// 这是展示树状结构的最佳实践。
function Entry({ entry, depth }) {
const [isExpanded, setIsExpanded] = useState(true); // 默认展开
// 根据是否有 children 判断是文件夹还是文件
const isDirectory = entry.children && entry.children.length > 0;
const handleToggle = () => {
setIsExpanded(!isExpanded);
};
// 文件夹排前面,文件排后面
const sortedChildren = isDirectory
? [...entry.children].sort((a, b) => {
const aIsDir = !!a.children;
const bIsDir = !!b.children;
return bIsDir - aIsDir; // true (1) - false (0) = 1, b会排前面
})
: null;
return (
<div>
<div
style={{ paddingLeft: `${depth * 20}px` }}
className="entry-item"
onClick={isDirectory ? handleToggle : undefined}
>
{isDirectory && <span>{isExpanded ? "📂" : "📁"} </span>}
{!isDirectory && <span>📄 </span>}
{entry.name}
</div>
{/* 递归渲染子节点 */}
{isExpanded && isDirectory && (
<div>
{sortedChildren.map((child) => (
<Entry key={child.id} entry={child} depth={depth + 1} />
))}
</div>
)}
</div>
);
}
// --- 3. 主应用组件 App ---
export default function App() {
const [result, setResult] = useState([]);
const [loading, setLoading] = useState(true); // 初始状态即为 loading
const [error, setError] = useState(null);
// --- 正确处理异步副作用 ---
useEffect(() => {
// 这是一个非常重要的模式:使用一个标志位来处理组件卸载的竞态条件。
// 这可以防止在组件卸载后,异步请求完成时调用 setState 导致的内存泄漏警告。
let isActive = true;
const loadData = async () => {
try {
// 在 effect 内部,我们不直接修改外部的 state,而是先获取数据
const data = await fetchData();
// 当 Promise resolve 后,检查组件是否仍然挂载
if (isActive) {
setResult(data);
setError(null);
}
} catch (err) {
if (isActive) {
setError(err.message);
}
} finally {
if (isActive) {
setLoading(false);
}
}
};
loadData();
// 这是 useEffect 的 cleanup 函数。它在组件卸载时,或下一次 effect 执行前运行。
// 在这里我们将标志位设为 false,中断任何可能的回调。
return () => {
isActive = false;
console.log("Cleanup: Component unmounted or effect re-ran.");
};
}, []); // 空依赖数组,确保 effect 只在组件挂载时运行一次
return (
<div className="App">
<h1>React File Explorer</h1>
{loading && <p>Loading...</p>}
{error && <p style={{ color: "red" }}>Error: {error}</p>}
{!loading && !error && (
<div>
{result.map((item) => (
// 使用稳定且唯一的 id 作为 key
<Entry key={item.id} entry={item} depth={0} />
))}
</div>
)}
</div>
);
}