<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>React App</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
index.html
style.css
tsx
main.tsx
tsx
app.tsx
package.json
tsx
list.tsx
现在支持上传本地图片了!
index.html
style.css
编辑器加载中
main.tsx
import { createRoot } from "react-dom/client"
import App from "./app.tsx"
const root = createRoot(document.getElementById("root"))
root.render(<App />)
编辑器加载中
app.tsx
import { type MouseEvent } from "react"
import confetti from "canvas-confetti"
import List from './list.tsx'
const App = () => {
return (
<>
<List />
</>
)
}
export default App
编辑器加载中
package.json
注意:新添加的依赖包首次加载可能会报错,稍后再次刷新即可
{
"dependencies": {
"react": "18.3.1",
"react-dom": "18.3.1",
"canvas-confetti": "1.9.3"
}
}
编辑器加载中
list.tsx
import { useCallback, useEffect, useRef, useState, useMemo, memo } from "react";
type ListType = {
background: string;
id: number; // 添加唯一ID
};
const listStyle = {
width: "100%",
height: "500px",
display: "flex",
justifyContent: "center",
alignItems: "center",
fontSize: "50px",
};
const getRandomInt = (max: number): number => {
return Math.floor(Math.random() * (max + 1));
};
const getRandomRgbColor = (): string => {
const r = getRandomInt(255);
const g = getRandomInt(255);
const b = getRandomInt(255);
return `rgb(${r}, ${g}, ${b})`;
};
// 使用React.memo包装列表项组件,避免不必要的重渲染
const ListItem = memo(({ item, index }: { item: ListType; index: number }) => {
return (
<div style={{ ...listStyle, background: item.background }}>
<div>{index}</div>
</div>
);
});
ListItem.displayName = 'ListItem';
const List = () => {
const [data, setData] = useState<ListType[]>([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const scrollRef = useRef<HTMLDivElement>(null);
const observer = useRef<IntersectionObserver>();
const loadingRef = useRef<HTMLDivElement>(null);
// 使用useMemo缓存数据列表的计算结果
const dataList = useMemo(() => {
return data.map((item, index) => (
<ListItem item={item} index={index} key={item.id} />
));
}, [data]);
const getList = useCallback(() => {
if (loading || !hasMore) return;
setLoading(true);
const items: ListType[] = [];
for (let i = 0; i < 10; i++) {
items.push({
background: getRandomRgbColor(),
id: Date.now() + i // 生成唯一ID
});
}
setTimeout(() => {
setData((prevData) => [...prevData, ...items]);
setLoading(false);
// 模拟数据加载完毕的情况
if (data.length > 40) {
setHasMore(false);
}
}, 1000);
}, [loading, hasMore, data.length]);
// 使用Intersection Observer API替代scroll事件
useEffect(() => {
if (loading || !hasMore) return;
const option = {
root: scrollRef.current,
rootMargin: '0px',
threshold: 0.1
};
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
getList();
}
}, option);
if (loadingRef.current) {
observer.current.observe(loadingRef.current);
}
return () => {
if (observer.current) {
observer.current.disconnect();
}
};
}, [getList, loading, hasMore]);
useEffect(() => {
// 初始化数据
if (data.length === 0) {
setData([{ background: "rgb(233, 32, 38)", id: Date.now() }]);
getList();
}
}, [data.length, getList]);
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
<h1 style={{ textAlign: "center", padding: "20px", margin: 0 }}>
优化后的无限滚动列表
</h1>
<div style={{ padding: "0 20px", fontSize: "14px", color: "#666" }}>
<p>性能优化:使用React.memo、useMemo、Intersection Observer API</p>
<p>已加载项目数: {data.length}</p>
</div>
<div
ref={scrollRef}
style={{
flex: 1,
overflow: "auto",
border: "1px solid #ccc",
borderRadius: "8px",
margin: "10px",
}}
>
{dataList}
<div ref={loadingRef} style={{ height: '1px' }}></div>
{loading && (
<div
style={{
width: "100%",
height: "100px",
textAlign: "center",
lineHeight: "100px",
fontSize: "20px",
background: "#f0f0f0",
}}
>
<div>加载中...</div>
</div>
)}
{!hasMore && (
<div
style={{
width: "100%",
height: "100px",
textAlign: "center",
lineHeight: "100px",
fontSize: "20px",
background: "#e0e0e0",
}}
>
<div>已加载全部数据</div>
</div>
)}
</div>
</div>
);
};
export default List
编辑器加载中
预览页面